diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 515f3bec49..99f47ea40c 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -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 diff --git a/backend/test/backend_tests/config_test.clj b/backend/test/backend_tests/config_test.clj index af06598962..74a159a703 100644 --- a/backend/test/backend_tests/config_test.clj +++ b/backend/test/backend_tests/config_test.clj @@ -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/"]