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_at is on or after 2026-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_at is 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_templates flag 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::CustomTemplates is a CE class that always returns true, with the SaaS logic in an EE override via prepend_mod. A module with def self. methods would not work here, prepend only 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::Fields only.
  • 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

  1. Simulate SaaS (GITLAB_SIMULATE_SAAS=1) and enable the flag:

    Feature.enable(:service_desk_restrict_custom_templates)
  2. Set up Service Desk on a project in a free root namespace, and add .gitlab/service_desk_templates/thank_you.md with recognizable content.

  3. Set the namespace created_at after the cutoff:

    project.root_namespace.update!(created_at: Date.new(2026, 9, 18))
  4. Create a ticket. The thank you email uses the default template, and log/application_json.log records one suppression entry.

  5. Set created_at before 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.

Edited by Mario Celi

Merge request reports

Loading
Loading