0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-21 06:02:32 -05:00

♻️ Fix some fundamental bugs on climit module

The climit previously of this commit is heavily used inside a
transactions, so in heavy contention operation such that file thumbnail
creation can cause a db pool exhaust.

This commit fixes this issue setting up a better resource limiting
mechanism that works outside the transactions so, contention will
no longer hold an open connection/transaction.

It also adds general improvement to the traceability to the climit
mechanism: it now properly logs the profile-id that is currently
cause some contention on specific resources.

It also add a general/root climit that is applied to all requests
so if someone start making abussive requests, we can clearly detect
it.
This commit is contained in:
Andrey Antukh 2024-01-27 22:33:52 +01:00
parent 658c26014b
commit a5c6d78ee5
12 changed files with 291 additions and 212 deletions

View file

@ -3,15 +3,26 @@
;; Optional: queue, ommited means Integer/MAX_VALUE
;; Optional: timeout, ommited means no timeout
;; Note: queue and timeout are excluding
{:update-file/by-profile
{:update-file/global {:permits 20}
:update-file/by-profile
{:permits 1 :queue 5}
:update-file/global {:permits 20}
:process-font/global {:permits 4}
:process-font/by-profile {:permits 1}
:derive-password/global {:permits 8}
:process-font/global {:permits 4}
:process-image/global {:permits 8}
:process-image/by-profile {:permits 1}
:auth/global {:permits 8}
:root/global
{:permits 40}
:root/by-profile
{:permits 10}
:file-thumbnail-ops/global
{:permits 20}
:file-thumbnail-ops/by-profile
{:permits 2}

View file

@ -322,9 +322,7 @@
::rpc/climit (ig/ref ::rpc/climit)
::rpc/rlimit (ig/ref ::rpc/rlimit)
::setup/templates (ig/ref ::setup/templates)
::props (ig/ref ::setup/props)
:pool (ig/ref ::db/pool)}
::props (ig/ref ::setup/props)}
:app.rpc.doc/routes
{:methods (ig/ref :app.rpc/methods)}

View file

@ -240,8 +240,7 @@
::mtx/metrics
::main/props]
:opt [::climit
::rlimit]
:req-un [::db/pool]))
::rlimit]))
(defmethod ig/init-key ::methods
[_ cfg]

View file

@ -21,26 +21,31 @@
[app.worker :as-alias wrk]
[clojure.edn :as edn]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.fs :as fs]
[integrant.core :as ig]
[promesa.core :as p]
[promesa.exec :as px]
[promesa.exec.bulkhead :as pbh])
(:import
clojure.lang.ExceptionInfo))
clojure.lang.ExceptionInfo
java.util.concurrent.atomic.AtomicLong))
(set! *warn-on-reflection* true)
(defn- id->str
[id]
(-> (str id)
(subs 1)))
([id]
(-> (str id)
(subs 1)))
([id key]
(if key
(str (-> (str id) (subs 1)) "/" key)
(id->str id))))
(defn- create-cache
[{:keys [::wrk/executor]}]
(letfn [(on-remove [key _ cause]
(let [[id skey] key]
(l/dbg :hint "destroy limiter" :id (id->str id) :key skey :reason (str cause))))]
(l/dbg :hint "disposed" :id (id->str id skey) :reason (str cause))))]
(cache/create :executor executor
:on-remove on-remove
:keepalive "5m")))
@ -81,132 +86,179 @@
(defn- create-limiter
[config [id skey]]
(l/dbg :hint "create limiter" :id (id->str id) :key skey)
(l/dbg :hint "created" :id (id->str id skey))
(pbh/create :permits (or (:permits config) (:concurrency config))
:queue (or (:queue config) (:queue-size config))
:timeout (:timeout config)
:type :semaphore))
(defn- invoke!
[config cache metrics id key f]
(if-let [limiter (cache/get cache [id key] (partial create-limiter config))]
(let [tpoint (dt/tpoint)
labels (into-array String [(id->str id)])
wrapped (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(l/trc :hint "acquired"
:id (id->str id)
:key key
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats)
:elapsed (dt/format-duration elapsed))
(defmacro ^:private measure-and-log!
[metrics mlabels stats id action limit-id limit-label profile-id elapsed]
`(let [mpermits# (:max-permits ~stats)
mqueue# (:max-queue ~stats)
permits# (:permits ~stats)
queue# (:queue ~stats)
queue# (- queue# mpermits#)
queue# (if (neg? queue#) 0 queue#)
level# (if (pos? queue#) :warn :trace)]
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels labels)
(try
(f)
(finally
(let [elapsed (tpoint)]
(l/trc :hint "finished"
:id (id->str id)
:key key
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats)
:elapsed (dt/format-duration elapsed)))))))
measure!
(fn [stats]
(mtx/run! metrics
:id :rpc-climit-queue
:val (:queue stats)
:labels labels)
(mtx/run! metrics
:id :rpc-climit-permits
:val (:permits stats)
:labels labels))]
(mtx/run! ~metrics
:id :rpc-climit-queue
:val queue#
:labels ~mlabels)
(try
(let [stats (pbh/get-stats limiter)]
(measure! stats)
(l/trc :hint "enqueued"
:id (id->str id)
:key key
:permits (:permits stats)
:queue (:queue stats)
:max-permits (:max-permits stats)
:max-queue (:max-queue stats))
(px/invoke! limiter wrapped))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]
(if (= :bulkhead-error type)
(mtx/run! ~metrics
:id :rpc-climit-permits
:val permits#
:labels ~mlabels)
(l/log level#
:hint ~action
:req ~id
:id ~limit-id
:label ~limit-label
:profile-id (str ~profile-id)
:permits permits#
:queue queue#
:max-permits mpermits#
:max-queue mqueue#
~@(if (some? elapsed)
[:elapsed `(dt/format-duration ~elapsed)]
[]))))
(def ^:private idseq (AtomicLong. 0))
(defn- invoke
[limiter metrics limit-id limit-key limit-label profile-id f params]
(let [tpoint (dt/tpoint)
limit-id (id->str limit-id limit-key)
mlabels (into-array String [limit-id])
stats (pbh/get-stats limiter)
id (.incrementAndGet ^AtomicLong idseq)]
(try
(measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil)
(px/invoke! limiter (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed)
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels mlabels)
(apply f params))))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]
(if (= :bulkhead-error type)
(let [elapsed (tpoint)]
(measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed)
(ex/raise :type :concurrency-limit
:code code
:hint "concurrency limit reached")
(throw cause))))
:hint "concurrency limit reached"
:cause cause))
(throw cause))))
(finally
(measure! (pbh/get-stats limiter)))))
(do
(l/wrn :hint "no limiter found" :id (id->str id))
(f))))
(finally
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MIDDLEWARE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def noop-fn (constantly nil))
(def ^:private noop-fn (constantly nil))
(def ^:private global-limits
[[:root/global noop-fn]
[:root/by-profile ::rpc/profile-id]])
(defn- get-limits
[cfg]
(when-let [ref (get cfg ::id)]
(cond
(keyword? ref)
[[ref]]
(and (vector? ref)
(keyword (first ref)))
[ref]
(and (vector? ref)
(vector? (first ref)))
(rseq ref)
:else
(throw (IllegalArgumentException. "unable to normalize limit")))))
(defn wrap
[{:keys [::rpc/climit ::mtx/metrics]} f {:keys [::id ::key-fn] :or {key-fn noop-fn} :as mdata}]
(if (and (some? climit) (some? id))
(let [cache (::cache climit)
config (::config climit)]
(if-let [config (get config id)]
(do
(l/dbg :hint "instrumenting method"
:limit (id->str id)
:service-name (::sv/name mdata)
:timeout (:timeout config)
:permits (:permits config)
:queue (:queue config)
:keyed? (not= key-fn noop-fn))
[{:keys [::rpc/climit ::mtx/metrics]} handler mdata]
(let [cache (::cache climit)
config (::config climit)
label (::sv/name mdata)]
(fn [cfg params]
(invoke! config cache metrics id (key-fn params) (partial f cfg params))))
(reduce (fn [handler [limit-id key-fn]]
(if-let [config (get config limit-id)]
(let [key-fn (or key-fn noop-fn)]
(l/dbg :hint "instrumenting method"
:method label
:limit (id->str limit-id)
:timeout (:timeout config)
:permits (:permits config)
:queue (:queue config)
:keyed (not= key-fn noop-fn))
(do
(l/wrn :hint "no config found for specified queue" :id (id->str id))
f)))
f))
(if (and (= key-fn ::rpc/profile-id)
(false? (::rpc/auth mdata true)))
;; We don't enforce by-profile limit on methods that does
;; not require authentication
handler
(fn [cfg params]
(let [limit-key (key-fn params)
cache-key [limit-id limit-key]
limiter (cache/get cache cache-key (partial create-limiter config))
profile-id (if (= key-fn ::rpc/profile-id)
limit-key
(get params ::rpc/profile-id))]
(invoke limiter metrics limit-id limit-key label profile-id handler [cfg params])))))
(do
(l/wrn :hint "no config found for specified queue" :id (id->str limit-id))
handler)))
handler
(concat global-limits (get-limits mdata)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; PUBLIC API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn configure
[{:keys [::rpc/climit]} id]
(us/assert! ::rpc/climit climit)
(assoc climit ::id id))
(defn- build-exec-chain
[{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f]
(let [config (get climit ::config)
cache (get climit ::cache)]
(defn run!
(reduce (fn [handler [limit-id limit-key :as ckey]]
(let [config (get config limit-id)]
(when-not config
(throw (IllegalArgumentException.
(str/ffmt "config not found for: %" limit-id))))
(fn [& params]
(let [limiter (cache/get cache ckey (partial create-limiter config))]
(invoke limiter metrics limit-id limit-key label profile-id handler params)))))
f
(get-limits cfg))))
(defn invoke!
"Run a function in context of climit.
Intended to be used in virtual threads."
([{:keys [::id ::cache ::config ::mtx/metrics]} f]
(if-let [config (get config id)]
(invoke! config cache metrics id nil f)
(f)))
([{:keys [::id ::cache ::config ::mtx/metrics]} f executor]
(let [f #(p/await! (px/submit! executor f))]
(if-let [config (get config id)]
(invoke! config cache metrics id nil f)
(f)))))
[{:keys [::executor] :as cfg} f & params]
(let [f (if (some? executor)
(fn [& params] (px/await! (px/submit! executor (fn [] (apply f params)))))
f)
f (build-exec-chain cfg f)]
(apply f params)))

View file

@ -21,6 +21,7 @@
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
@ -39,7 +40,7 @@
;; ---- COMMAND: login with password
(defn login-with-password
[{:keys [::db/pool] :as cfg} {:keys [email password] :as params}]
[cfg {:keys [email password] :as params}]
(when-not (or (contains? cf/flags :login)
(contains? cf/flags :login-with-password))
@ -47,7 +48,7 @@
:code :login-disabled
:hint "login is disabled in this instance"))
(letfn [(check-password [conn profile password]
(letfn [(check-password [cfg profile password]
(if (= (:password profile) "!")
(ex/raise :type :validation
:code :account-without-password
@ -57,10 +58,10 @@
(l/trc :hint "updating profile password"
:id (str (:id profile))
:email (:email profile))
(profile/update-profile-password! conn (assoc profile :password password)))
(profile/update-profile-password! cfg (assoc profile :password password)))
(:valid result))))
(validate-profile [conn profile]
(validate-profile [cfg profile]
(when-not profile
(ex/raise :type :validation
:code :wrong-credentials))
@ -70,7 +71,7 @@
(when (:is-blocked profile)
(ex/raise :type :restriction
:code :profile-blocked))
(when-not (check-password conn profile password)
(when-not (check-password cfg profile password)
(ex/raise :type :validation
:code :wrong-credentials))
(when-let [deleted-at (:deleted-at profile)]
@ -78,27 +79,29 @@
(ex/raise :type :validation
:code :wrong-credentials)))
profile)]
profile)
(db/with-atomic [conn pool]
(let [profile (->> (profile/get-profile-by-email conn email)
(validate-profile conn)
(profile/strip-private-attrs))
(login [{:keys [::db/conn] :as cfg}]
(let [profile (->> (profile/get-profile-by-email conn email)
(validate-profile cfg)
(profile/strip-private-attrs))
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))
invitation (when-let [token (:invitation-token params)]
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
;; invitation because invitations matches exactly; and user can't login with other email and
;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)}
(assoc profile :is-admin (let [admins (cf/get :admins)]
(contains? admins (:email profile)))))]
(-> response
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
;; invitation because invitations matches exactly; and user can't login with other email and
;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)}
(assoc profile :is-admin (let [admins (cf/get :admins)]
(contains? admins (:email profile)))))]
(-> response
(rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))]
(db/tx-run! cfg login)))
(def schema:login-with-password
[:map {:title "login-with-password"}
@ -110,6 +113,7 @@
"Performs authentication using penpot password."
{::rpc/auth false
::doc/added "1.15"
::climit/id :auth/global
::sm/params schema:login-with-password}
[cfg params]
(login-with-password cfg params))
@ -149,7 +153,8 @@
(sv/defmethod ::recover-profile
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:recover-profile}
::sm/params schema:recover-profile
::climit/id :auth/global}
[cfg params]
(recover-profile cfg params))
@ -360,7 +365,6 @@
{::audit/type "fact"
::audit/name "register-profile-retry"
::audit/profile-id id}))
(cond
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
@ -402,7 +406,6 @@
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
(def schema:register-profile
[:map {:title "register-profile"}
[:token schema:token]
@ -411,7 +414,8 @@
(sv/defmethod ::register-profile
{::rpc/auth false
::doc/added "1.15"
::sm/params schema:register-profile}
::sm/params schema:register-profile
::climit/id :auth/global}
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg ::db/conn conn)

View file

@ -285,12 +285,10 @@
(sv/defmethod ::create-file-object-thumbnail
{::doc/added "1.19"
::doc/module :files
::climit/id :file-thumbnail-ops/by-profile
::climit/key-fn ::rpc/profile-id
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
[:file-thumbnail-ops/global]]
::rtry/enabled true
::rtry/when rtry/conflict-exception?
::audit/skip true
::sm/params schema:create-file-object-thumbnail}
@ -332,8 +330,8 @@
{::doc/added "1.19"
::doc/module :files
::doc/deprecated "1.20"
::climit/id :file-thumbnail-ops
::climit/key-fn ::rpc/profile-id
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
[:file-thumbnail-ops/global]]
::audit/skip true}
[cfg {:keys [::rpc/profile-id file-id object-id]}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
@ -408,8 +406,8 @@
{::doc/added "1.19"
::doc/module :files
::audit/skip true
::climit/id :file-thumbnail-ops
::climit/key-fn ::rpc/profile-id
::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id]
[:file-thumbnail-ops/global]]
::rtry/enabled true
::rtry/when rtry/conflict-exception?
::sm/params schema:create-file-thumbnail}

View file

@ -35,7 +35,8 @@
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.set :as set]))
[clojure.set :as set]
[promesa.exec :as px]))
;; --- SCHEMA
@ -132,8 +133,8 @@
;; database.
(sv/defmethod ::update-file
{::climit/id :update-file/by-profile
::climit/key-fn ::rpc/profile-id
{::climit/id [[:update-file/by-profile ::rpc/profile-id]
[:update-file/global]]
::webhooks/event? true
::webhooks/batch-timeout (dt/duration "2m")
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
@ -232,13 +233,9 @@
(defn- update-file*
[{:keys [::db/conn ::wrk/executor] :as cfg}
{:keys [profile-id file changes session-id ::created-at skip-validate] :as params}]
(let [;; Process the file data in the CLIMIT context; scheduling it
;; to be executed on a separated executor for avoid to do the
;; CPU intensive operation on vthread.
update-fdata-fn (partial update-file-data cfg file changes skip-validate)
file (-> (climit/configure cfg :update-file/global)
(climit/run! update-fdata-fn executor))]
(let [;; Process the file data on separated thread for avoid to do
;; the CPU intensive operation on vthread.
file (px/invoke! executor (partial update-file-data cfg file changes skip-validate))]
(db/insert! conn :file-change
{:id (uuid/next)
@ -306,7 +303,6 @@
(fmg/migrate-file))
file)
;; WARNING: this ruins performance; maybe we need to find
;; some other way to do general validation
libs (when (and (or (contains? cf/flags :file-validation)

View file

@ -16,7 +16,7 @@
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.climit :as-alias climit]
[app.rpc.commands.files :as files]
[app.rpc.commands.projects :as projects]
[app.rpc.commands.teams :as teams]
@ -26,7 +26,8 @@
[app.storage :as sto]
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]))
[app.worker :as-alias wrk]
[promesa.exec :as px]))
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"})
@ -87,6 +88,8 @@
(sv/defmethod ::create-font-variant
{::doc/added "1.18"
::climit/id [[:process-font/by-profile ::rpc/profile-id]
[:process-font/global]]
::webhooks/event? true
::sm/params schema:create-font-variant}
[cfg {:keys [::rpc/profile-id team-id] :as params}]
@ -100,7 +103,7 @@
(create-font-variant cfg (assoc params :profile-id profile-id))))))
(defn create-font-variant
[{:keys [::sto/storage ::db/conn] :as cfg} {:keys [data] :as params}]
[{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}]
(letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf"))
@ -152,9 +155,7 @@
:otf-file-id (:id otf)
:ttf-file-id (:id ttf)}))]
(let [data (-> (climit/configure cfg :process-font/global)
(climit/run! (partial generate-missing! data)
(::wrk/executor cfg)))
(let [data (px/invoke! executor (partial generate-missing! data))
assets (persist-fonts-files! data)
result (insert-font-variant! assets)]
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))

View file

@ -27,7 +27,8 @@
[app.worker :as-alias wrk]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[datoteka.io :as io]))
[datoteka.io :as io]
[promesa.exec :as px]))
(def default-max-file-size
(* 1024 1024 10)) ; 10 MiB
@ -56,20 +57,25 @@
:opt-un [::id]))
(sv/defmethod ::upload-file-media-object
{::doc/added "1.17"}
{::doc/added "1.17"
::climit/id [[:process-image/by-profile ::rpc/profile-id]
[:process-image/global]]}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(media/validate-media-type! content)
(media/validate-media-size! content)
(let [object (db/run! cfg #(create-file-media-object % params))
props {:name (:name params)
:file-id file-id
:is-local (:is-local params)
:size (:size content)
:mtype (:mtype content)}]
(with-meta object
{::audit/replace-props props}))))
(db/run! cfg (fn [cfg]
(let [object (create-file-media-object cfg params)
props {:name (:name params)
:file-id file-id
:is-local (:is-local params)
:size (:size content)
:mtype (:mtype content)}]
(with-meta object
{::audit/replace-props props}))))))
(defn- big-enough-for-thumbnail?
"Checks if the provided image info is big enough for
@ -144,12 +150,10 @@
(assoc ::image (process-main-image info)))))
(defn create-file-media-object
[{:keys [::sto/storage ::db/conn ::wrk/executor] :as cfg}
[{:keys [::sto/storage ::db/conn ::wrk/executor]}
{:keys [id file-id is-local name content]}]
(let [result (-> (climit/configure cfg :process-image/global)
(climit/run! (partial process-image content) executor))
(let [result (px/invoke! executor (partial process-image content))
image (sto/put-object! storage (::image result))
thumb (when-let [params (::thumb result)]
(sto/put-object! storage params))]
@ -183,7 +187,7 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(files/check-edition-permissions! pool profile-id file-id)
(db/run! cfg #(create-file-media-object-from-url % params))))
(create-file-media-object-from-url cfg (assoc params :profile-id profile-id))))
(defn download-image
[{:keys [::http/client]} uri]
@ -235,7 +239,16 @@
params (-> params
(assoc :content content)
(assoc :name (or name (:filename content))))]
(create-file-media-object cfg params)))
;; NOTE: we use the climit here in a dynamic invocation because we
;; don't want saturate the process-image limit with IO (download
;; of external image)
(-> cfg
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
[:process-image/global]])
(assoc ::climit/profile-id (:profile-id params))
(assoc ::climit/label "create-file-media-object-from-url")
(climit/invoke! db/run! cfg create-file-media-object params))))
;; --- Clone File Media object (Upload and create from url)

View file

@ -28,7 +28,8 @@
[app.util.services :as sv]
[app.util.time :as dt]
[app.worker :as-alias wrk]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[promesa.exec :as px]))
(declare check-profile-existence!)
(declare decode-row)
@ -137,25 +138,24 @@
[:old-password {:optional true} [:maybe [::sm/word-string {:max 500}]]]]))
(sv/defmethod ::update-profile-password
{:doc/added "1.0"
{::doc/added "1.0"
::sm/params schema:update-profile-password
::sm/result :nil}
::climit/id :auth/global}
[cfg {:keys [::rpc/profile-id password] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id password] :as params}]
(db/with-atomic [conn pool]
(let [cfg (assoc cfg ::db/conn conn)
profile (validate-password! cfg (assoc params :profile-id profile-id))
session-id (::session/id params)]
(db/tx-run! cfg (fn [cfg]
(let [profile (validate-password! cfg (assoc params :profile-id profile-id))
session-id (::session/id params)]
(when (= (str/lower (:email profile))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
(when (= (str/lower (:email profile))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
(update-profile-password! conn (assoc profile :password password))
(invalidate-profile-session! cfg profile-id session-id)
nil)))
(update-profile-password! cfg (assoc profile :password password))
(invalidate-profile-session! cfg profile-id session-id)
nil))))
(defn- invalidate-profile-session!
"Removes all sessions except the current one."
@ -173,10 +173,10 @@
profile))
(defn update-profile-password!
[conn {:keys [id password] :as profile}]
[{:keys [::db/conn] :as cfg} {:keys [id password] :as profile}]
(when-not (db/read-only? conn)
(db/update! conn :profile
{:password (auth/derive-password password)}
{:password (derive-password cfg password)}
{:id id})
nil))
@ -203,6 +203,7 @@
(defn update-profile-photo
[{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}]
(let [photo (upload-photo cfg params)
profile (db/get-by-id pool :profile profile-id ::sql/for-update true)]
@ -241,8 +242,11 @@
(defn upload-photo
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}]
(let [params (-> (climit/configure cfg :process-image/global)
(climit/run! (partial generate-thumbnail! file) executor))]
(let [params (-> cfg
(assoc ::climit/id :process-image/global)
(assoc ::climit/label "upload-photo")
(assoc ::climit/executor executor)
(climit/invoke! generate-thumbnail! file))]
(sto/put-object! storage params)))
@ -438,17 +442,13 @@
(into {} (filter (fn [[k _]] (simple-ident? k))) props))
(defn derive-password
[cfg password]
[{:keys [::wrk/executor]} password]
(when password
(-> (climit/configure cfg :derive-password/global)
(climit/run! (partial auth/derive-password password)
(::wrk/executor cfg)))))
(px/invoke! executor (partial auth/derive-password password))))
(defn verify-password
[cfg password password-data]
(-> (climit/configure cfg :derive-password/global)
(climit/run! (partial auth/verify-password password password-data)
(::wrk/executor cfg))))
[{:keys [::wrk/executor]} password password-data]
(px/invoke! executor (partial auth/verify-password password password-data)))
(defn decode-row
[{:keys [props] :as row}]

View file

@ -71,6 +71,7 @@
:enable-email-verification
:enable-smtp
:enable-quotes
:enable-rpc-climit
:enable-feature-fdata-pointer-map
:enable-feature-fdata-objets-map
:enable-feature-components-v2

View file

@ -319,6 +319,12 @@
::message (delay ~message)})
nil)))
(defmacro log
[level & params]
`(do
(log! ::logger ~(str *ns*) ::level ~level ~@params)
nil))
(defmacro info
[& params]
`(do