Restrict Service Desk templates for new free namespaces
What does this MR do and why?
Related to https://gitlab.com/gitlab-org/gitlab/-/work_items/627335
Restricts custom Service Desk email templates (thank_you, new_note,
new_participant) for new free and trial root namespaces on GitLab.com, so
those namespaces fall back to the default GitLab email templates.
Follow-up to the Service Desk email rate limiting work (!253313 (merged)): rate limits cap how many emails a namespace sends, this caps how much they can be customized.
When the restriction applies
Only when all of these hold, so the default is always to keep templates working:
- GitLab.com only. Self-managed and FOSS are unaffected by construction.
- Root namespace
created_atis on or after2026-09-17(the 19.4 release date). Namespaces created before the cutoff keep custom templates unconditionally, so nobody loses a template they already use.created_atis immutable, so a later trial or downgrade cannot pull a namespace into the restriction. - The namespace is not on a paid, non-trial plan.
- The
service_desk_restrict_custom_templatesflag is enabled.
The boundary is paid vs free, not group vs personal
Worth calling out explicitly, since "free and trial namespaces" is easy to read as group-scoped: a group with no subscription is restricted too. And personal namespaces cannot hold a subscription on GitLab.com, so a personal namespace created after the cutoff stays restricted with no upgrade path. That is intentional, but it means the docs cannot say "upgrade to a paid plan to enable customization" as a blanket statement, so the note distinguishes the two cases.
Implementation notes
ServiceDesk::CustomTemplatesis a CE class that always returnstrue, with the SaaS logic in an EE override viaprepend_mod. A module withdef self.methods would not work here,prependonly reaches the instance method chain.- The mailer resolves the template before checking the restriction, so the suppression log only fires when a template actually existed rather than on every email these namespaces send.
- Suppression logging uses standard
Labkit::Fieldsonly. - Tickets, comments, and participants are unaffected. Only the template lookup changes.
Known limitations
- Attacker-controlled content still reaches the default templates (ticket title in the subject, description and note text in the body). This raises the bar, it does not eliminate impersonation.
- Project transfers can move a project between a restricted and an
unrestricted namespace. Any
created_at-based rule has an equivalent gap. - A DB-backed per-namespace flag was suggested as a longer-term alternative to the date constant. Not done here: the fixed date needs no backfill and no new state, but it is worth revisiting if per-namespace overrides or admin control become useful.
How to set up and validate locally
-
Simulate SaaS (
GITLAB_SIMULATE_SAAS=1) and enable the flag:Feature.enable(:service_desk_restrict_custom_templates) -
Set up Service Desk on a project in a free root namespace, and add
.gitlab/service_desk_templates/thank_you.mdwith recognizable content. -
Set the namespace
created_atafter the cutoff:project.root_namespace.update!(created_at: Date.new(2026, 9, 18)) -
Create a ticket. The thank you email uses the default template, and
log/application_json.logrecords one suppression entry. -
Set
created_atbefore the cutoff, or disable the flag, and confirm the custom template is used again.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.