mirror of
https://github.com/penpot/penpot.git
synced 2026-09-01 17:49:08 +00:00
✨ Add several improvements for demo profile creation mechanism (#11257)
* ⚡ Optimize demo user setup for performance tests Use UUID-based demo emails to prevent concurrent profile collisions.\nUse fast PBKDF2 hashing for demo profiles while keeping regular user hashing unchanged.\nAdd focused coverage for hashing, email uniqueness, and the feature flag.\n\nAI-assisted-by: gpt-5.6-luna * 🐛 Harden font upload test setup Report upload-session errors before chunk validation. Skip chunk uploads when the session ID is invalid. Remove unnecessary Mockery state from the foreign-font test. AI-assisted-by: gpt-5.6-luna * ✨ Add demo profile purge task Schedule delayed deletion for demo profiles through the worker system. Restore normal profile filtering and cover the purge handler with tests. AI-assisted-by: gpt-5.6-luna
This commit is contained in:
parent
92c2079ae2
commit
3df039abc3
@ -14,10 +14,21 @@
|
||||
:iterations 3
|
||||
:parallelism 2})
|
||||
|
||||
(def ^:private weak-options
|
||||
{:alg :pbkdf2+sha256
|
||||
:iterations 100})
|
||||
|
||||
(defn derive-password
|
||||
[password]
|
||||
(hashers/derive password default-options))
|
||||
|
||||
(defn derive-password-weak
|
||||
"Derives a password using a fast algorithm (pbkdf2+sha256, 100 iterations).
|
||||
Intended for demo users only — they are already gated behind the
|
||||
`demo-users` config flag which is disabled in production."
|
||||
[password]
|
||||
(hashers/derive password weak-options))
|
||||
|
||||
(defn verify-password
|
||||
[attempt password]
|
||||
(try
|
||||
|
||||
@ -392,6 +392,8 @@
|
||||
|
||||
:delete-object
|
||||
(ig/ref :app.tasks.delete-object/handler)
|
||||
:demo-purge
|
||||
(ig/ref :app.tasks.demo-purge/handler)
|
||||
:process-webhook-event
|
||||
(ig/ref ::webhooks/process-event-handler)
|
||||
:run-webhook
|
||||
@ -429,6 +431,9 @@
|
||||
:app.tasks.delete-object/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.tasks.demo-purge/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
:app.tasks.file-gc/handler
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
::sto/storage (ig/ref ::sto/storage)}
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
(ns app.rpc.commands.demo
|
||||
"A demo specific mutations."
|
||||
(:require
|
||||
[app.auth :refer [derive-password]]
|
||||
[app.auth :refer [derive-password-weak]]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.loggers.audit :as audit]
|
||||
@ -17,6 +17,7 @@
|
||||
[app.rpc.commands.auth :as auth]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as wrk]
|
||||
[buddy.core.codecs :as bc]
|
||||
[buddy.core.nonce :as bn]))
|
||||
|
||||
@ -34,8 +35,8 @@
|
||||
:code :demo-users-not-allowed
|
||||
:hint "Demo users are disabled by config."))
|
||||
|
||||
(let [sem (System/currentTimeMillis)
|
||||
email (str "demo-" sem ".demo@example.com")
|
||||
(let [sem (uuid/next)
|
||||
email (str "demo-" sem "@demo.example.com")
|
||||
fullname (str "Demo User " sem)
|
||||
|
||||
password (-> (bn/random-bytes 16)
|
||||
@ -46,13 +47,17 @@
|
||||
:fullname fullname
|
||||
:is-active true
|
||||
:is-demo true
|
||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
||||
:password (derive-password password)
|
||||
:password (derive-password-weak password)
|
||||
:props {}}
|
||||
profile (db/tx-run! cfg (fn [cfg]
|
||||
(->> (auth/create-profile cfg params)
|
||||
(auth/create-profile-rels cfg))))]
|
||||
|
||||
(wrk/submit! (-> cfg
|
||||
(assoc ::wrk/task :demo-purge)
|
||||
(assoc ::wrk/delay (cf/get-deletion-delay))
|
||||
(assoc ::wrk/params {:profile-id (:id profile)})))
|
||||
|
||||
(with-meta {:email email
|
||||
:password password}
|
||||
{::audit/profile-id (:id profile)})))
|
||||
|
||||
|
||||
@ -141,9 +141,7 @@
|
||||
(defn get-profile
|
||||
"Get profile by id. Throws not-found exception if no profile found."
|
||||
[conn id & {:as opts}]
|
||||
;; NOTE: We need to set ::db/remove-deleted to false because demo profiles
|
||||
;; are created with a set deleted-at value
|
||||
(-> (db/get-by-id conn :profile id (assoc opts ::db/remove-deleted false))
|
||||
(-> (db/get-by-id conn :profile id opts)
|
||||
(decode-row)))
|
||||
|
||||
;; --- MUTATION: Update Profile (own)
|
||||
|
||||
41
backend/src/app/tasks/demo_purge.clj
Normal file
41
backend/src/app/tasks/demo_purge.clj
Normal file
@ -0,0 +1,41 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns app.tasks.demo-purge
|
||||
"Task handler for delayed demo profile deletion. Submitted at demo
|
||||
creation time with a delay matching the configured deletion-delay."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.worker :as wrk]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
|
||||
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props]}]
|
||||
(let [profile-id (get props :profile-id)
|
||||
now (ct/now)]
|
||||
|
||||
(l/trc :hint "demo-purge" :profile-id (str profile-id))
|
||||
|
||||
;; Mark the profile for immediate deletion
|
||||
(db/tx-run! cfg
|
||||
(fn [{:keys [::db/conn] :as cfg}]
|
||||
(db/update! conn :profile
|
||||
{:deleted-at now}
|
||||
{:id profile-id}
|
||||
{::db/return-keys false})
|
||||
(wrk/submit!
|
||||
(-> cfg
|
||||
(assoc ::wrk/task :delete-object)
|
||||
(assoc ::wrk/params {:object :profile
|
||||
:deleted-at now
|
||||
:id profile-id}))))))))
|
||||
47
backend/test/backend_tests/demo_test.clj
Normal file
47
backend/test/backend_tests/demo_test.clj
Normal file
@ -0,0 +1,47 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.demo-test
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.db :as db]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.tasks.demo-purge :as demo-purge]
|
||||
[app.worker :as wrk]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest demo-profile-created-without-deleted-at
|
||||
(let [profile (th/create-profile* 999 {:is-demo true})]
|
||||
(t/is (true? (:is-demo profile)))
|
||||
(t/is (nil? (:deleted-at profile)))
|
||||
(t/is (some? (:id profile)))))
|
||||
|
||||
(t/deftest get-profile-finds-demo-user-without-override
|
||||
(let [profile (th/create-profile* 998 {:is-demo true})
|
||||
found (db/run! th/*pool*
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(profile/get-profile conn (:id profile))))]
|
||||
(t/is (some? found))
|
||||
(t/is (= (:id profile) (:id found)))))
|
||||
|
||||
(t/deftest demo-purge-handler-submits-delete-object
|
||||
(let [profile (th/create-profile* 996 {:is-demo true})
|
||||
handler (ig/init-key :app.tasks.demo-purge/handler
|
||||
{::db/pool th/*pool*})
|
||||
submitted (atom nil)]
|
||||
(with-redefs [wrk/submit! (fn [& {:keys [::wrk/task ::wrk/params]}]
|
||||
(reset! submitted {:task task :params params}))]
|
||||
(handler {:props {:profile-id (:id profile)
|
||||
:deleted-at (ct/now)}}))
|
||||
(t/is (= :delete-object (:task @submitted)))
|
||||
(t/is (= :profile (:object (:params @submitted))))
|
||||
(t/is (= (:id profile) (:id (:params @submitted))))
|
||||
(t/is (some? (:deleted-at (:params @submitted))))))
|
||||
40
backend/test/backend_tests/rpc_demo_test.clj
Normal file
40
backend/test/backend_tests/rpc_demo_test.clj
Normal file
@ -0,0 +1,40 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||
|
||||
(ns backend-tests.rpc-demo-test
|
||||
(:require
|
||||
[app.auth :as auth]
|
||||
[app.config :as cf]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
;; Capture the real verifier before the shared test fixture replaces it.
|
||||
(def verify-password* auth/verify-password)
|
||||
|
||||
(t/deftest weak-password-hash-verifies
|
||||
(let [password "DemoPassword123!"
|
||||
hashed (auth/derive-password-weak password)]
|
||||
(t/is (:valid (verify-password* password hashed)))))
|
||||
|
||||
(t/deftest create-demo-profile-uses-unique-uuid-email
|
||||
(with-redefs [cf/flags (conj cf/flags :demo-users)]
|
||||
(let [first-result (th/command! {::th/type :create-demo-profile})
|
||||
second-result (th/command! {::th/type :create-demo-profile})
|
||||
first-profile (:result first-result)
|
||||
second-profile (:result second-result)]
|
||||
(t/is (nil? (:error first-result)))
|
||||
(t/is (nil? (:error second-result)))
|
||||
(t/is (re-matches #"demo-[0-9a-fA-F-]+@demo\.example\.com"
|
||||
(:email first-profile)))
|
||||
(t/is (not= (:email first-profile) (:email second-profile))))))
|
||||
|
||||
(t/deftest create-demo-profile-requires-feature-flag
|
||||
(with-redefs [cf/flags (disj cf/flags :demo-users)]
|
||||
(let [{:keys [error]} (th/command! {::th/type :create-demo-profile})]
|
||||
(t/is (th/ex-of-code? error :demo-users-not-allowed)))))
|
||||
@ -59,8 +59,13 @@
|
||||
(let [out (th/command! {::th/type :create-upload-session
|
||||
::rpc/profile-id (:id prof)
|
||||
:total-chunks total-chunks})]
|
||||
(t/is (nil? (:error out)))
|
||||
(:session-id (:result out))))
|
||||
(let [session-id (:session-id (:result out))]
|
||||
(t/is (nil? (:error out))
|
||||
(str "create-upload-session failed: "
|
||||
(some-> (:error out) ex-data)))
|
||||
(t/is (uuid? session-id)
|
||||
(str "create-upload-session returned an invalid session-id: " session-id))
|
||||
session-id)))
|
||||
|
||||
(defn- upload-font-chunked!
|
||||
"Splits `font-bytes` into chunks of `chunk-size` bytes, creates an upload
|
||||
@ -68,14 +73,15 @@
|
||||
[prof ^bytes font-bytes mtype chunk-size]
|
||||
(let [chunks (split-bytes-into-chunks font-bytes chunk-size)
|
||||
session-id (create-upload-session! prof (count chunks))]
|
||||
(doseq [[idx chunk-data] (map-indexed vector chunks)]
|
||||
(let [mfile (make-chunk-mfile chunk-data mtype)
|
||||
out (th/command! {::th/type :upload-chunk
|
||||
::rpc/profile-id (:id prof)
|
||||
:session-id session-id
|
||||
:index idx
|
||||
:content mfile})]
|
||||
(t/is (nil? (:error out)))))
|
||||
(when (uuid? session-id)
|
||||
(doseq [[idx chunk-data] (map-indexed vector chunks)]
|
||||
(let [mfile (make-chunk-mfile chunk-data mtype)
|
||||
out (th/command! {::th/type :upload-chunk
|
||||
::rpc/profile-id (:id prof)
|
||||
:session-id session-id
|
||||
:index idx
|
||||
:content mfile})]
|
||||
(t/is (nil? (:error out))))))
|
||||
session-id))
|
||||
|
||||
(defn- assert-font-variant-result
|
||||
@ -613,43 +619,42 @@
|
||||
;; N2-07: A user with edit permissions on their own team must not be
|
||||
;; able to create a font variant using a font-id that already belongs
|
||||
;; to another team (BOLA / CWE-639).
|
||||
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
|
||||
(let [prof1 (th/create-profile* 1 {:is-active true})
|
||||
prof2 (th/create-profile* 2 {:is-active true})
|
||||
team1 (:default-team-id prof1)
|
||||
team2 (:default-team-id prof2)
|
||||
font-id (uuid/custom 10 999)
|
||||
data (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
(io/read*))]
|
||||
(let [prof1 (th/create-profile* 1 {:is-active true})
|
||||
prof2 (th/create-profile* 2 {:is-active true})
|
||||
team1 (:default-team-id prof1)
|
||||
team2 (:default-team-id prof2)
|
||||
font-id (uuid/custom 10 999)
|
||||
data (-> (io/resource "backend_tests/test_files/font-1.ttf")
|
||||
(io/read*))]
|
||||
|
||||
;; prof1 creates a font variant in team1 with font-id
|
||||
(let [session-id (upload-font-chunked! prof1 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof1)
|
||||
:team-id team1
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (nil? (:error out))))
|
||||
;; prof1 creates a font variant in team1 with font-id
|
||||
(let [session-id (upload-font-chunked! prof1 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof1)
|
||||
:team-id team1
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 400
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (nil? (:error out))))
|
||||
|
||||
;; prof2 tries to create a variant using the same font-id but
|
||||
;; in team2, which must be rejected because font-id belongs to team1
|
||||
(let [session-id (upload-font-chunked! prof2 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof2)
|
||||
:team-id team2
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 700
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (some? (:error out)))
|
||||
(t/is (= :not-found (-> out :error ex-data :type)))
|
||||
(t/is (= :object-not-found (-> out :error ex-data :code)))))))
|
||||
;; prof2 tries to create a variant using the same font-id but
|
||||
;; in team2, which must be rejected because font-id belongs to team1
|
||||
(let [session-id (upload-font-chunked! prof2 data "font/ttf" (* 4 1024 1024))
|
||||
params {::th/type :create-font-variant
|
||||
::rpc/profile-id (:id prof2)
|
||||
:team-id team2
|
||||
:font-id font-id
|
||||
:font-family "SharedFont"
|
||||
:font-weight 700
|
||||
:font-style "normal"
|
||||
:uploads {"font/ttf" session-id}}
|
||||
out (th/command! params)]
|
||||
(t/is (some? (:error out)))
|
||||
(t/is (= :not-found (-> out :error ex-data :type)))
|
||||
(t/is (= :object-not-found (-> out :error ex-data :code))))))
|
||||
|
||||
(t/deftest get-font-variants-nonexistent-file
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user