The reference pages were plain definition lists, so no individual setting had
an anchor. There was no way to link someone to HELPDESK_ENABLE_ATTACHMENTS, and
a setting name mentioned in prose was just literal text that nothing checked.
Registers a "setting" object type in docs/conf.py through add_object_type,
which is the built-in shortcut for what Django's own documentation does with a
full custom domain. The Python domain would have worked too, but these are not
attributes of any module we ship: they live in the deployer's settings.py, and
putting them in the py inventory would mix them into the Python index the day
we add autodoc for the models.
The 81 entries in settings.rst become setting directives, each with an anchor,
a permalink and an index entry, and :setting:`NAME` now resolves. Five mentions
in prose became cross-references. The default value moves out of the term and
into the body, so it no longer has to be squeezed into the heading.
The standalone.rst tables were left as they are on purpose. They document the
Docker image's environment variables, which are a separate namespace, and two
of the names listed there do not exist as settings at all.
The three management commands the walkthrough tells you to run are now
documented with program and option directives, their flags taken from each
command's add_arguments rather than from the surrounding prose. The
--debug_to_stdout example was wrapped in ** inside a literal block, so the
asterisks were rendering verbatim.
HELPDESK_ALWAYS_SAVE_INCOMING_EMAIL_MESSAGE was listed under Discontinued
Settings, but src/helpdesk/settings.py still reads it and src/helpdesk/email.py
still acts on it, gated on HELPDESK_ENABLE_ATTACHMENTS exactly as its
description claims. It moves back up with the other attachment settings. It is
the only one of the seventeen names in that section that still exists in the
code.
Also replaces the single backticks that were rendering as italics rather than
as literals in api.rst, install.rst, standalone.rst and settings.rst, and
carries the epub_exclude_files change that silences the two logo warnings
blocking the Read the Docs build.
Django helpdesk logo shown on left and the search bar is widened and sits as the center of the nav. Nav bar now stays fixed on top on page scroll and has slight tweaks for better layout. There is a very soft light background in both color modes
Raised while triaging GHSA-q42j-c994-653f, a report that the login page accepts
unlimited authentication attempts. It is not a defect here, since
helpdesk/views/login.py is a thin wrapper around
django.contrib.auth.views.LoginView and Django deliberately does not rate limit
authentication. But the reporter's second point stands: nothing in our
documentation says that hardening is the deployer's job.
That matters more for this project than for most Django applications, because
the default shape is unusual. We ship a public-facing login page and a public
ticket submission form, so a normal installation is reachable from the internet
rather than sitting on an intranet. Telling people to handle it in front of the
application is correct advice we had never given.
Adds a Production hardening section to docs/configuration.rst covering rate
limiting, password validators, attachment serving, transport and turning off
unused surface, with a cross-reference from the standalone production list.
Two things documented that were not written down anywhere: setting the
SECURE_PROXY_SSL_HEADER environment variable makes helpdesk/settings.py set
Django's SECURE_PROXY_SSL_HEADER along with SESSION_COOKIE_SECURE and
CSRF_COOKIE_SECURE, and HELPDESK_API_ENABLED and
HELPDESK_SUBMIT_A_TICKET_PUBLIC can remove the REST API and the public form
entirely.
`accessible_tickets()` granted a ticket assigned to the requesting user whatever
its queue, on the grounds that `can_access_ticket()` does the same and the API
should not be stricter than the UI. The premise was wrong: `ticket_perm_check()`
tests `can_access_queue()` first and raises before reaching that branch, so the
UI denies such a ticket and the assignment exception is unreachable there.
Granting it in the API therefore left the API more permissive than the
interface, which is the shape of the problem being fixed rather than a
concession to it. Removed, and the test now asserts that both sides deny it.
Found by the reporter while verifying the remediation.
`accessible_tickets()` claimed to be the queryset form of
`can_access_ticket()` but was built on `get_queues()`, and the two differ:
`get_queues()` also returns every queue accepting public submissions, while
`can_access_queue()`, which the staff views consult before serving a ticket,
does not. The API was therefore wider than the permission check it was meant to
mirror, and a restricted account could read a public-queue ticket through the
API that the UI refuses to show it.
The new test asserts both sides rather than a bare status code, so it records
that the API and the UI agree rather than hardcoding today's answer.
`restrict_relation()` now raises instead of returning quietly when the field is
missing or is not a relation. Skipping silently would drop an authorization
check the moment a field is renamed, with no signal anywhere, which is the one
failure mode it must not have.
Both raised by @DavidVadnais in review.
`merged_to` is a second writable relation pointing at a Ticket, so narrowing
`queue` alone left a way across the boundary: a restricted staff member could
PATCH one of their own tickets with `merged_to` set to a foreign ticket id. That
returned 200 and persisted the link, which both confirms the foreign ticket
exists and writes a reference into it. Now rejected with 400, with a test that
fails without the change.
`assigned_to` is deliberately left alone. It points at a User rather than
anything queue-scoped, so whatever the right rule is for assigning tickets to
arbitrary users, it is not part of this advisory.
The second point was whether tickets in queues that accept public submissions
should stay visible to every staff member. They should, and they do:
`get_queues()` includes those queues for everyone and the staff UI behaves the
same way, so reusing that helper means the API inherits the rule rather than
inventing a different one. Added a test that pins it, so the grant reads as
intended rather than as something we missed.
Fixes GHSA-g9r9-54m2-m6g5, reported by @b00kawa.
With HELPDESK_ENABLE_PER_QUEUE_STAFF_PERMISSION enabled, the staff UI keeps a
restricted staff account inside its granted queues through
HelpdeskUser.can_access_queue() and can_access_ticket(). The API did not.
TicketViewSet, FollowUpViewSet and FollowUpAttachmentViewSet were guarded only
by DRF's IsAdminUser, which checks is_staff and nothing else, over querysets of
Ticket.objects.all(), FollowUp.objects.all() and FollowUpAttachment.objects.all().
A staff member restricted to one queue could therefore list and read every
ticket, read non-public follow-ups, read attachment contents, modify tickets and
create follow-ups anywhere, while the same account got a 403 for the same ticket
through the UI.
The three querysets are now scoped through accessible_tickets(), which mirrors
can_access_ticket() rather than being merely stricter: the UI also grants access
to a ticket assigned to the user even outside their queues, so the API grants
the same set. With per-queue permissions off, or for a superuser, get_queues()
returns everything and the filter is a no-op, so the default configuration is
unchanged.
Scoping the querysets covers reads and updates, since DRF resolves both through
get_queryset(). Creation names its target by primary key instead, so the
writable relations are narrowed too: `ticket` on follow-ups and `followup` on
attachments. `queue` on tickets is narrowed as well, though that path was
already blocked by TicketForm restricting its choices; doing it at the
serializer stops an authorization decision from depending on a form the API
happens to reuse.
Issue #1335 had already noted that the API does not consult can_access_queue().
The fix in PR #1338 covered user.py, feeds.py and staff.py but not api.py, so
the gap survived for four months. The regression tests go in the same file that
PR #1338 added to, so the API is not overlooked there again. Six of them fail
without this change, one per reported vector.
the will updated created timestamp and won't update anytime later. This created field won't be editable in the admin as well. The will automatically update the modified field anytime a change is made so no need to set values in method. Note: This change produces a simple migration.
We introduce simple type hints to Ticket model. Property decorator is used on model methods for simplicity removing the non pythonic method names starting with an underscore. Slight focus on removing conditionals to map and reduce verbosity
GitHub Support told us in August 2026 that CVE requests run about three weeks,
in order of arrival, with no expediting, and that Dependabot alerts are driven
from the advisory rather than from the CVE assignment.
That last point changes the advice. #1386 said to decide deliberately whether to
wait for the identifier before publishing. There is nothing to decide: waiting
delays the alerts that actually protect users and buys only control of an
external record. Step 2 now says to request it and stop thinking about it.
The caveat about another CNA assigning one from the published advisory stays,
since that is what happened to GHSA-q46c-8w98-fq2g, but it is now framed as
something to accept rather than something to avoid by waiting.