You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Upgrading
Single sign-on is an install-level switch, off by default
(ESCALATED["ACCESS"]["sso"], env ESCALATED_SSO; ADR
2026-10-29-configurable-access-features). Nothing ever signed anyone in
through SSO, so off is how every install already behaved; while it is off
Admin > Settings > SSO and every /support/sso/* route answer 404. To turn
it on: python manage.py migrate escalated (0038), give the IdP the
endpoints from the SSO page, make sure a SAML install has the IdP
certificate, decide on ESCALATED["SSO"]["create_users"] and ["sync_roles"], then set the switch. See docs/access-control.md.
escalated.services.sso_service.SsoService is rewritten on signxml, Authlib and PyJWT (new dependencies): validate_saml_assertion and validate_jwt_token are replaced by validate_saml_response and validate_jwt, which check far more (see below). The SSO settings page now
validates the fields each provider needs and lists the endpoints for the IdP.
Two-factor authentication is now an install-level switch, off by default
(ESCALATED["ACCESS"]["two_factor"], env ESCALATED_TWO_FACTOR; ADR
2026-10-29-configurable-access-features). Nothing ever asked for the code, so
off is how every install already behaved; while it is off the enrolment
screen answers 404. Run python manage.py migrate escalated (0037 adds last_used_step and stores the secret and recovery codes sealed), then set
the switch. Users who enrolled before are challenged from their next visit.
Set ESCALATED_TWO_FACTOR_REQUIRED=true (or the setting) only after telling
staff. See docs/access-control.md.
The enrolment endpoints (admin/settings/two-factor/setup|confirm|disable/)
now answer the shared page with a redirect and flash, and JSON to API
clients (Accept: application/json), as escalated-laravel does; they were
JSON only, and admin only.
New dependency: pyotp (TOTP); the hand-rolled HMAC is gone.
Roles & permissions is now an install-level switch, off by default
(ESCALATED["ACCESS"]["roles_permissions"], env ESCALATED_ROLES_PERMISSIONS; ADR 2026-10-29-configurable-access-features).
Roles were stored but only the newsletter screens checked them, so off is how
almost every install already behaved. While it is off the roles screens
(/support/admin/roles/*) answer 404, and the newsletter screens and plugin
item permissions no longer check roles (the admin check decides). Installs
that relied on those must turn the switch on. To enforce roles: run python manage.py seed_permissions, give every staff user a role
(python manage.py assign_role you@example.com admin), then turn the switch
on. An admin without a role is refused everywhere but their own two-factor
screen. See docs/access-control.md.
The shared escalated.permissions prop is ['*'] for staff while the switch
is off, and escalated.features carries roles_permissions, two_factor
and sso.
With SDK plugins on (ESCALATED['SDK_ENABLED']), run python manage.py escalated_plugin_cron every minute (cron, Celery Beat,
django-crontab) so SDK plugins' cron schedules run. Without it they never
run; nothing else changes.
Run python manage.py migrate escalated. The knowledge_base_public
setting now defaults on, as in escalated-laravel, so a new install
serves its knowledge base (customer pages and widget articles) to guests.
Migration 0036_knowledge_base_public_default_on keeps existing installs
as they were: if the install already has Escalated data (any ticket,
department, article, article category or chat session, or 0001_initial
applied more than an hour earlier) and no stored knowledge_base_public,
it stores 0, so the knowledge base stays private. A value an admin already
saved is left alone. To open the knowledge base to guests afterwards, turn
on "public knowledge base" in the admin Settings page.
Plugin UI extensions are shared as escalated.plugins (menuItems, pageComponents, dashboardWidgets), the shape the shared frontend reads.
Before, nothing was shared, so plugin menus, widgets and slots never
appeared, and EscalatedInertiaShareMiddleware called inertia-django's share() with positional arguments, which it refuses. Admin menu items go
to admins only, items whose permission the viewer lacks are left out, and
SDK plugins' widgets and components are included. {} when there is
nothing to show.
Plugins receive the canonical domain events.ticket.created, reply.created, note.created, ticket.status_changed and the rest of
the event vocabulary go, once the change commits, to Python plugins
(add_action("ticket.created", lambda payload: ...)) and SDK plugins, with
the plugin contract's payload: the ticket flat and under ticket, plus the
event's extras; replies flat plus reply and ticket. SDK plugins used to
get only ten of them, with ticket_id instead of the ticket. The Python hook
names (ticket_created, ...) still fire for Python plugins. A failing
plugin is logged and never fails the change.
ctx.tickets.query and ctx.replies.query only accept the plugin
contract's filter keys. Tickets: id, reference, status, priority, channel, ticket_type, department_id, requester_id, contact_id, assigned_to. Replies: ticket_id, is_internal, type. Any other key
(including Django lookups such as pk__gt or assigned_to__email), or a
non-scalar value, raises instead of reaching objects.filter(). Results are
capped at 500 rows.
Added
Admin > Users roles picker. While roles & permissions is on, the users
screen sends roles ({id, name, slug}, by name) and each user's role slugs,
and PUT C:/Program Files/Git/support/admin/users/<id>/roles/ ({role_ids: [...]}, needs role.manage) replaces a user's
roles, as escalated-laravel's escalated.admin.users.roles. 404 while the
switch is off.
Sign-in through SSO when switched on, with the provider chosen in Admin >
Settings > SSO. SAML checks the assertion signature against the configured
certificate, Issuer, Audience, Destination/Recipient, validity windows,
InResponseTo against requests issued in the last 10 minutes, and replay;
OAuth uses a session-bound state and PKCE; JWT pins the algorithm and
requires exp, aud and a single-use jti. Users are matched by email,
created only with create_users, and given the asserted role only with
roles on and sync_roles.
Two-factor authentication is enforced when switched on. A staff user
with a confirmed enrolment must pass Auth/TwoFactorChallenge
(/support/two-factor/challenge/; TOTP code or single-use recovery code;
replayed codes refused; 5 attempts per minute) in their session before any
admin or agent view; JSON requests get 403 {two_factor_required: true}.
Enrolment is open to agents as well as admins. Admins can require enrolment
(ESCALATED["TWO_FACTOR"]["REQUIRED"] or the two_factor_required
setting), and python manage.py reset_two_factor <user> clears a lost
device. Pages get escalated.two_factor_required.
Roles & permissions are enforced when switched on. Every admin, agent and
staff API view needs its permission from the user's Escalated roles as well
as the admin or agent check (map in escalated/access.py; unmapped staff URLs
are refused and a test checks every one is mapped). Status changes to resolved/closed/reopened also need ticket.close, bulk actions need
each action's permission, internal API replies need reply.create_internal,
and API tokens are checked as their owner. Failing answers 403.
python manage.py assign_role <user> <role>.
SDK plugin cron schedules run.python manage.py escalated_plugin_cron
(every minute; --at picks the minute) sends cron {plugin, schedule} to
the plugin runtime for each schedule in a plugin's manifest cronSchedules
that is due that minute: five-field expressions in UTC (or @hourly, @daily, ...). Each runs as no user within 30 s; a failing schedule is
logged and never stops the others, and one that cannot be read is logged
and skipped. As with actions, every SDK plugin the runtime loaded takes part.
SDK widget badges. An SDK widget with a badge function (manifest hasBadge: true) carries badge in escalated.plugins.dashboardWidgets:
the plugin's answer to badge {plugin, component}, asked as the viewer
(ctx.currentUser is the signed-in user during that call) within 5 s; a
number or a string, null when it fails. Widgets without one carry no badge key.
Changed
knowledge_base_public defaults on for new installs (was off), and the
admin Settings page shows it on when nothing is stored.
Security
Plugin ticket and reply queries passed the filter to objects.filter(), so
an SDK plugin could filter on any field or follow relations (such as the
assignee's password hash). Keys are now whitelisted.
ctx.emit drops hook names that would impersonate a canonical event, a
legacy alias of one, or a plugin lifecycle action.