🔧 Normalize the tenant to lowercase (#12843)

An operator writing `PENPOT_TENANT=ACME` and one writing `acme` name the
same tenant: the value is compared as an exact string in the keys and
rows it lands in, and one uppercase letter would otherwise split one
tenant into two.

The value is normalized when the configuration is read, so nothing
downstream has to care about case, and the validated form accepts only
lowercase labels from then on.

AI-assisted-by: deepseek-v4.1-flash
This commit is contained in:
Andrey Antukh 2026-11-05 08:45:01 +01:00 committed by GitHub
parent eb9594a614
commit 9d898b8b31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 28 additions and 3 deletions

View File

@ -111,8 +111,14 @@
:ssrf-extra-blocked-cidrs #{}})
(def schema:tenant
"Tenant identifier: hostname-label-style (letters, digits and
hyphens). It is interpolated into Redis keys separated by dots (msgbus
"Tenant identifier: hostname-label-style, lowercase (letters, digits
and hyphens). The value is normalized to lowercase when the
configuration is read, so an operator writing `PENPOT_TENANT=ACME` and
one writing `acme` name the same tenant: the value is compared as an
exact string in the keys and rows it lands in, and one uppercase
letter would otherwise split one tenant into two.
It is interpolated into Redis keys separated by dots (msgbus
topics, rate-limit buckets, exporter job keys, which are also matched
with a glob pattern), so `.` and whitespace are rejected at startup
because one tenant would then be able to read or overwrite another's
@ -125,7 +131,14 @@
tenant asking for it, and a tenant that has one would have to be
renamed. Tenants using those characters must be renamed before
upgrading."
[:re #"^[A-Za-z0-9-]+$"])
[:re #"^[a-z0-9-]+$"])
(defn- normalize-tenant
"Lowercase the configured tenant before it is validated, so
`PENPOT_TENANT=ACME` and `acme` mean the same tenant everywhere."
[config]
(cond-> config
(string? (:tenant config)) (update :tenant str/lower)))
(def schema:config
(do #_sm/optional-keys
@ -401,6 +414,7 @@
[& {:keys [prefix default] :or {prefix "penpot"}}]
(->> (read-env prefix)
(merge default)
(normalize-tenant)
(decode-config)))
(def version

View File

@ -6,10 +6,21 @@
(ns backend-tests.config-test
(:require
[app.common.schema :as sm]
[app.common.time :as ct]
[app.config :as cf]
[clojure.test :as t]))
(t/deftest tenant-is-normalized-to-lowercase
(t/testing "an uppercase tenant names the same tenant"
(t/is (= "acme" (:tenant (#'cf/normalize-tenant {:tenant "ACME"}))))
(t/is (= "acme" (:tenant (#'cf/normalize-tenant {:tenant "acme"})))))
(t/testing "a config without a tenant is left untouched"
(t/is (= {:x 1} (#'cf/normalize-tenant {:x 1}))))
(t/testing "the validated form is lowercase-only"
(t/is (not (sm/validate cf/schema:tenant "ACME")))
(t/is (sm/validate cf/schema:tenant "acme"))))
(t/deftest get-public-uri-normalizes-base
(t/testing "trailing slash is ensured with and without subpath"
(doseq [[base expected] [["http://localhost:3449" "http://localhost:3449/"]