mirror of
https://github.com/penpot/penpot.git
synced 2025-06-01 07:41:40 +02:00
292 lines
9.2 KiB
Clojure
292 lines
9.2 KiB
Clojure
;; 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
|
|
|
|
(ns app.rpc
|
|
(:require
|
|
[app.auth.ldap :as-alias ldap]
|
|
[app.common.data :as d]
|
|
[app.common.exceptions :as ex]
|
|
[app.common.logging :as l]
|
|
[app.common.schema :as sm]
|
|
[app.common.spec :as us]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.http :as-alias http]
|
|
[app.http.access-token :as actoken]
|
|
[app.http.client :as-alias http.client]
|
|
[app.http.session :as session]
|
|
[app.loggers.audit :as audit]
|
|
[app.main :as-alias main]
|
|
[app.metrics :as mtx]
|
|
[app.msgbus :as-alias mbus]
|
|
[app.rpc.climit :as climit]
|
|
[app.rpc.cond :as cond]
|
|
[app.rpc.helpers :as rph]
|
|
[app.rpc.retry :as retry]
|
|
[app.rpc.rlimit :as rlimit]
|
|
[app.setup :as-alias setup]
|
|
[app.storage :as-alias sto]
|
|
[app.util.inet :as inet]
|
|
[app.util.services :as sv]
|
|
[app.util.time :as dt]
|
|
[clojure.spec.alpha :as s]
|
|
[cuerdas.core :as str]
|
|
[integrant.core :as ig]
|
|
[promesa.core :as p]
|
|
[yetti.request :as yreq]
|
|
[yetti.response :as yres]))
|
|
|
|
(s/def ::profile-id ::us/uuid)
|
|
|
|
(defn- default-handler
|
|
[_]
|
|
(p/rejected (ex/error :type :not-found)))
|
|
|
|
(defn- handle-response-transformation
|
|
[response request mdata]
|
|
(reduce (fn [response transform-fn]
|
|
(transform-fn request response))
|
|
response
|
|
(::response-transform-fns mdata)))
|
|
|
|
(defn- handle-before-comple-hook
|
|
[response mdata]
|
|
(doseq [hook-fn (::before-complete-fns mdata)]
|
|
(ex/ignoring (hook-fn)))
|
|
response)
|
|
|
|
(defn- handle-response
|
|
[request result]
|
|
(let [mdata (meta result)
|
|
response (if (fn? result)
|
|
(result request)
|
|
(let [result (rph/unwrap result)]
|
|
{::yres/status (::http/status mdata 200)
|
|
::yres/headers (::http/headers mdata {})
|
|
::yres/body result}))]
|
|
(-> response
|
|
(handle-response-transformation request mdata)
|
|
(handle-before-comple-hook mdata))))
|
|
|
|
(defn get-external-session-id
|
|
[request]
|
|
(when-let [session-id (yreq/get-header request "x-external-session-id")]
|
|
(when-not (or (> (count session-id) 256)
|
|
(= session-id "null")
|
|
(str/blank? session-id))
|
|
session-id)))
|
|
|
|
(defn- get-external-event-origin
|
|
[request]
|
|
(when-let [origin (yreq/get-header request "x-event-origin")]
|
|
(when-not (or (> (count origin) 256)
|
|
(= origin "null")
|
|
(str/blank? origin))
|
|
origin)))
|
|
|
|
(defn- rpc-handler
|
|
"Ring handler that dispatches cmd requests and convert between
|
|
internal async flow into ring async flow."
|
|
[methods {:keys [params path-params method] :as request}]
|
|
(let [handler-name (:type path-params)
|
|
etag (yreq/get-header request "if-none-match")
|
|
profile-id (or (::session/profile-id request)
|
|
(::actoken/profile-id request))
|
|
|
|
ip-addr (inet/parse-request request)
|
|
session-id (get-external-session-id request)
|
|
event-origin (get-external-event-origin request)
|
|
|
|
data (-> params
|
|
(assoc ::handler-name handler-name)
|
|
(assoc ::ip-addr ip-addr)
|
|
(assoc ::request-at (dt/now))
|
|
(assoc ::external-session-id session-id)
|
|
(assoc ::external-event-origin event-origin)
|
|
(assoc ::session/id (::session/id request))
|
|
(assoc ::cond/key etag)
|
|
(cond-> (uuid? profile-id)
|
|
(assoc ::profile-id profile-id)))
|
|
|
|
data (vary-meta data assoc ::http/request request)
|
|
handler-fn (get methods (keyword handler-name) default-handler)]
|
|
|
|
(when (and (or (= method :get)
|
|
(= method :head))
|
|
(not (str/starts-with? handler-name "get-")))
|
|
(ex/raise :type :restriction
|
|
:code :method-not-allowed
|
|
:hint "method not allowed for this request"))
|
|
|
|
(binding [cond/*enabled* true]
|
|
(let [response (handler-fn data)]
|
|
(handle-response request response)))))
|
|
|
|
(defn- wrap-metrics
|
|
"Wrap service method with metrics measurement."
|
|
[{:keys [::mtx/metrics ::metrics-id]} f mdata]
|
|
(let [labels (into-array String [(::sv/name mdata)])]
|
|
(fn [cfg params]
|
|
(let [tp (dt/tpoint)]
|
|
(try
|
|
(f cfg params)
|
|
(finally
|
|
(mtx/run! metrics
|
|
:id metrics-id
|
|
:val (inst-ms (tp))
|
|
:labels labels)))))))
|
|
|
|
(defn- wrap-authentication
|
|
[_ f mdata]
|
|
(fn [cfg params]
|
|
(let [profile-id (::profile-id params)]
|
|
(if (and (::auth mdata true) (not (uuid? profile-id)))
|
|
(ex/raise :type :authentication
|
|
:code :authentication-required
|
|
:hint "authentication required for this endpoint")
|
|
(f cfg params)))))
|
|
|
|
(defn- wrap-db-transaction
|
|
[_ f mdata]
|
|
(if (::db/transaction mdata)
|
|
(fn [cfg params]
|
|
(db/tx-run! cfg f params))
|
|
f))
|
|
|
|
(defn- wrap-audit
|
|
[_ f mdata]
|
|
(if (or (contains? cf/flags :webhooks)
|
|
(contains? cf/flags :audit-log))
|
|
(if-not (::audit/skip mdata)
|
|
(fn [cfg params]
|
|
(let [result (f cfg params)]
|
|
(->> (audit/prepare-event cfg mdata params result)
|
|
(audit/submit! cfg))
|
|
result))
|
|
f)
|
|
f))
|
|
|
|
(defn- wrap-spec-conform
|
|
[_ f mdata]
|
|
;; NOTE: skip spec conform operation on rpc methods that already
|
|
;; uses malli validation mechanism.
|
|
(if (contains? mdata ::sm/params)
|
|
f
|
|
(if-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))]
|
|
(fn [cfg params]
|
|
(f cfg (us/conform spec params)))
|
|
f)))
|
|
|
|
(defn- wrap-params-validation
|
|
[_ f mdata]
|
|
(if-let [schema (::sm/params mdata)]
|
|
(let [validate (sm/validator schema)
|
|
explain (sm/explainer schema)
|
|
decode (sm/decoder schema sm/json-transformer)
|
|
encode (sm/encoder schema sm/json-transformer)]
|
|
(fn [cfg params]
|
|
(let [params (decode params)]
|
|
(if (validate params)
|
|
(let [result (f cfg params)]
|
|
(if (instance? clojure.lang.IObj result)
|
|
(vary-meta result assoc :encode/json encode)
|
|
result))
|
|
(let [params (d/without-qualified params)]
|
|
(ex/raise :type :validation
|
|
:code :params-validation
|
|
::sm/explain (explain params)))))))
|
|
f))
|
|
|
|
(defn- wrap-all
|
|
[cfg f mdata]
|
|
(as-> f $
|
|
(wrap-db-transaction cfg $ mdata)
|
|
(cond/wrap cfg $ mdata)
|
|
(retry/wrap-retry cfg $ mdata)
|
|
(climit/wrap cfg $ mdata)
|
|
(wrap-metrics cfg $ mdata)
|
|
(rlimit/wrap cfg $ mdata)
|
|
(wrap-audit cfg $ mdata)
|
|
(wrap-spec-conform cfg $ mdata)
|
|
(wrap-params-validation cfg $ mdata)
|
|
(wrap-authentication cfg $ mdata)))
|
|
|
|
(defn- wrap
|
|
[cfg f mdata]
|
|
(l/trc :hint "register method" :name (::sv/name mdata))
|
|
(let [f (wrap-all cfg f mdata)]
|
|
(partial f cfg)))
|
|
|
|
(defn- process-method
|
|
[cfg [vfn mdata]]
|
|
[(keyword (::sv/name mdata)) [mdata (wrap cfg vfn mdata)]])
|
|
|
|
(defn- resolve-command-methods
|
|
[cfg]
|
|
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
|
|
(->> (sv/scan-ns
|
|
'app.rpc.commands.access-token
|
|
'app.rpc.commands.audit
|
|
'app.rpc.commands.auth
|
|
'app.rpc.commands.feedback
|
|
'app.rpc.commands.fonts
|
|
'app.rpc.commands.binfile
|
|
'app.rpc.commands.comments
|
|
'app.rpc.commands.demo
|
|
'app.rpc.commands.files
|
|
'app.rpc.commands.files-create
|
|
'app.rpc.commands.files-share
|
|
'app.rpc.commands.files-temp
|
|
'app.rpc.commands.files-update
|
|
'app.rpc.commands.files-snapshot
|
|
'app.rpc.commands.files-thumbnails
|
|
'app.rpc.commands.ldap
|
|
'app.rpc.commands.management
|
|
'app.rpc.commands.media
|
|
'app.rpc.commands.profile
|
|
'app.rpc.commands.projects
|
|
'app.rpc.commands.search
|
|
'app.rpc.commands.teams
|
|
'app.rpc.commands.verify-token
|
|
'app.rpc.commands.viewer
|
|
'app.rpc.commands.webhooks)
|
|
(map (partial process-method cfg))
|
|
(into {}))))
|
|
|
|
(defmethod ig/pre-init-spec ::methods [_]
|
|
(s/keys :req [::session/manager
|
|
::http.client/client
|
|
::db/pool
|
|
::mbus/msgbus
|
|
::ldap/provider
|
|
::sto/storage
|
|
::mtx/metrics
|
|
::setup/props]
|
|
:opt [::climit
|
|
::rlimit]))
|
|
|
|
(defmethod ig/init-key ::methods
|
|
[_ cfg]
|
|
(let [cfg (d/without-nils cfg)]
|
|
(resolve-command-methods cfg)))
|
|
|
|
(s/def ::methods
|
|
(s/map-of keyword? (s/tuple map? fn?)))
|
|
|
|
(s/def ::routes vector?)
|
|
|
|
(defmethod ig/pre-init-spec ::routes [_]
|
|
(s/keys :req [::methods
|
|
::db/pool
|
|
::setup/props
|
|
::session/manager]))
|
|
|
|
(defmethod ig/init-key ::routes
|
|
[_ {:keys [::methods] :as cfg}]
|
|
(let [methods (update-vals methods peek)]
|
|
[["/rpc" {:middleware [[session/authz cfg]
|
|
[actoken/authz cfg]]}
|
|
["/command/:type" {:handler (partial rpc-handler methods)}]]]))
|