screentinker/server/routes
ScreenTinker 983bee31b7 SSO-only: close the backdoor, the unilateral disable, and the fresh-install fail-open
Three HIGH findings from the QA round. Each was demonstrated end to end against a
running server, and each is now refused there.

ENFORCEMENT PROTECTED A DOMAIN, NOT AN ORGANIZATION

ssoOnlyForEmail answers about an address's domain, so any account in the tenant at an
outside address kept password login — a contractor, an MSP, the one address nobody
remembered. And it could be manufactured: POST /api/admin/users accepts workspace_admin
and creates a LOCAL password account at any address bound to that workspace. A review
created backdoor@notacme.test, logged in with the password, landed in the SSO-only org,
and used it to create another. Enforcement is now keyed on MEMBERSHIP as well as domain
(ssoOnlyForUser), and that route refuses to mint password accounts into an SSO-only
organization at all. platform_admin keeps both, as the operator break-glass.

THE APPROVAL WORKFLOW WAS DECORATIVE

`sso_only` is honoured only while a provider is enabled and a domain is verified, so
`PUT {enabled:false}`, `PUT {email_domains:""}` and `DELETE` each switched enforcement
off — with sso_only still reading true, no request filed and the operator never told.
The delete variant additionally rewrites every federated account to `local`, after
which a password reset takes over accounts the identity provider was supposed to own.
Anyone who could file a request could simply turn the provider off instead. All three
now refuse with sso_only_locked when nothing else would still enforce, and say to ask
for approval.

FRESH INSTALLS FAILED THE MIGRATION AND FAILED OPEN

The ALTER adding organizations.sso_only sat in the column-migration array, which runs
BEFORE the multi-tenancy migration that creates the table: `[migrate] FAILED … no such
table: organizations`, one line among ~85. The instance then ran its whole first boot
with the SSO settings screen 500ing and ssoOnlyForEmail catching `no such column` and
answering "not required" — password login proceeding for an organization that had
switched it off. It self-healed on the second boot, which is what made it easy to miss.
The column is now added after the table exists, and the catch distinguishes "this
instance has no per-org SSO" (null, so single-tenant installs keep working) from drift
on a table that DOES exist (throw). Login treats an undeterminable answer as "required"
rather than letting a 500 escape or letting the login through.

Verified live, all four refused with enforcement intact and the operator still able to
sign in. 1609 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bvjey4FNam49MN7ybjcq6A
2026-08-11 07:23:28 -05:00
..
activity.js Phase 2.1: tenancy middleware, permission helpers, JWT workspace context, frontend + backend role-rename compat 2026-05-11 20:02:00 -05:00
admin.js SSO-only: close the backdoor, the unilateral disable, and the fresh-install fail-open 2026-08-11 07:23:28 -05:00
agency.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
ai.js feat(ai): separate optional image API key (#41) 2026-06-09 13:47:47 -05:00
assignments.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
auth.js SSO-only: close the backdoor, the unilateral disable, and the fresh-install fail-open 2026-08-11 07:23:28 -05:00
billing.js feat(#146): billing:read scoped token — dual-path auth for the Usage Report (Option C) 2026-07-01 21:16:21 -05:00
contact.js fix(landing): replace broken Custom pricing card with enterprise contact form 2026-05-14 13:52:24 -05:00
content.js QA: close four ways a control or an asset lied about itself 2026-08-06 16:12:29 -05:00
device-groups.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
devices.js BrightSign: report IPv6, the attached display and the active video mode 2026-08-10 15:30:40 -05:00
folders.js Phase 2.2c: content_folders gets workspace_id (schema + backfill); folders.js scoped; content.js folder-move strict same-workspace 2026-05-11 21:04:03 -05:00
kiosk.js Validate kiosk style values as CSS rather than as HTML 2026-07-27 22:28:47 -05:00
layouts.js Save a layout by diffing its zones, not by deleting and re-inserting them 2026-07-30 20:51:28 -05:00
media.js feat: transition engine — GL wipes across web, Tizen & Android (+ image↔video) (#204) 2026-07-20 16:45:32 -05:00
org-sso.js SSO-only: close the backdoor, the unilateral disable, and the fresh-install fail-open 2026-08-11 07:23:28 -05:00
pip.js Hold overlay pushes to the same write check as every other fleet action 2026-07-30 20:46:03 -05:00
player-debug.js Add player debug overlay and server-side error telemetry sink 2026-05-15 15:20:42 -05:00
playlists.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
provisioning.js fix(api): consolidate device pairing to /pair, remove vestigial bare endpoint (#90) 2026-06-12 20:13:16 -05:00
reports.js Phase 2.2g: reports.js scoped to workspace_id; fixes pre-existing /export and /uptime cross-tenant leaks 2026-05-11 21:36:54 -05:00
schedules.js Default a video playlist item to the clip's own length (#237) 2026-08-06 09:36:53 -05:00
status.js #240: stop the morning wave buying itself a blocking checkpoint 2026-08-06 20:22:21 -05:00
stripe.js feat(stripe): enable promotion codes on checkout sessions (#227) 2026-07-23 23:44:18 -05:00
subscription.js Initial open source release 2026-04-08 12:14:53 -05:00
teams.js feat(teams): temporarily disable Teams API while feature is redesigned 2026-05-12 13:30:55 -05:00
tokens.js feat(agency): per-token upload folder — auto-created, subtree-confined (#158) (#171) 2026-07-12 21:23:25 -05:00
video-walls.js Describe a portrait video wall as portrait, and stop a wall hiding its screens 2026-08-06 09:46:31 -05:00
white-label.js fix(security): patch quick-win findings from the codebase review 2026-06-08 19:02:19 -05:00
widgets.js Stop shrinking hand-written text widgets into illegibility 2026-07-30 20:28:28 -05:00
workspaces.js fix(workspaces): use APP_URL env var for invite-accept URL generation 2026-05-17 15:26:07 -05:00