mirror of
https://github.com/penpot/penpot.git
synced 2025-05-12 00:46:37 +02:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
e13bceeb59
7 changed files with 117 additions and 156 deletions
|
@ -41,33 +41,26 @@
|
||||||
|
|
||||||
(defn clean-props
|
(defn clean-props
|
||||||
[{:keys [profile-id] :as event}]
|
[{:keys [profile-id] :as event}]
|
||||||
(letfn [(clean-common [props]
|
(let [invalid-keys #{:session-id
|
||||||
(-> props
|
:password
|
||||||
(dissoc :session-id)
|
:old-password
|
||||||
(dissoc :password)
|
:token}
|
||||||
(dissoc :old-password)
|
xform (comp
|
||||||
(dissoc :token)))
|
(remove (fn [kv]
|
||||||
|
(qualified-keyword? (first kv))))
|
||||||
(clean-profile-id [props]
|
(remove (fn [kv]
|
||||||
(cond-> props
|
(contains? invalid-keys (first kv))))
|
||||||
(= profile-id (:profile-id props))
|
(remove (fn [[k v]]
|
||||||
(dissoc :profile-id)))
|
(and (= k :profile-id)
|
||||||
|
(= v profile-id))))
|
||||||
(clean-complex-data [props]
|
(filter (fn [[_ v]]
|
||||||
(reduce-kv (fn [props k v]
|
|
||||||
(cond-> props
|
|
||||||
(or (string? v)
|
(or (string? v)
|
||||||
|
(keyword? v)
|
||||||
(uuid? v)
|
(uuid? v)
|
||||||
(boolean? v)
|
(boolean? v)
|
||||||
(number? v))
|
(number? v)))))]
|
||||||
(assoc k v)
|
|
||||||
|
|
||||||
(keyword? v)
|
(update event :props #(into {} xform %))))
|
||||||
(assoc k (name v))))
|
|
||||||
{}
|
|
||||||
props))]
|
|
||||||
|
|
||||||
(update event :props #(-> % clean-common clean-profile-id clean-complex-data))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HTTP Handler
|
;; HTTP Handler
|
||||||
|
@ -82,11 +75,11 @@
|
||||||
(s/def ::timestamp dt/instant?)
|
(s/def ::timestamp dt/instant?)
|
||||||
(s/def ::context (s/map-of ::us/keyword any?))
|
(s/def ::context (s/map-of ::us/keyword any?))
|
||||||
|
|
||||||
(s/def ::event
|
(s/def ::frontend-event
|
||||||
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
|
(s/keys :req-un [::type ::name ::props ::timestamp ::profile-id]
|
||||||
:opt-un [::context]))
|
:opt-un [::context]))
|
||||||
|
|
||||||
(s/def ::events (s/every ::event))
|
(s/def ::frontend-events (s/every ::event))
|
||||||
|
|
||||||
(defmethod ig/init-key ::http-handler
|
(defmethod ig/init-key ::http-handler
|
||||||
[_ {:keys [executor pool] :as cfg}]
|
[_ {:keys [executor pool] :as cfg}]
|
||||||
|
@ -98,7 +91,7 @@
|
||||||
(when (contains? cf/flags :audit-log)
|
(when (contains? cf/flags :audit-log)
|
||||||
(let [events (->> (:events params)
|
(let [events (->> (:events params)
|
||||||
(remove #(not= profile-id (:profile-id %)))
|
(remove #(not= profile-id (:profile-id %)))
|
||||||
(us/conform ::events))
|
(us/conform ::frontend-events))
|
||||||
ip-addr (parse-client-ip request)
|
ip-addr (parse-client-ip request)
|
||||||
cfg (-> cfg
|
cfg (-> cfg
|
||||||
(assoc :source "frontend")
|
(assoc :source "frontend")
|
||||||
|
@ -147,9 +140,14 @@
|
||||||
(defmethod ig/pre-init-spec ::collector [_]
|
(defmethod ig/pre-init-spec ::collector [_]
|
||||||
(s/keys :req-un [::db/pool ::wrk/executor]))
|
(s/keys :req-un [::db/pool ::wrk/executor]))
|
||||||
|
|
||||||
(def event-xform
|
(s/def ::ip-addr string?)
|
||||||
|
(s/def ::backend-event
|
||||||
|
(s/keys :req-un [::type ::name ::profile-id]
|
||||||
|
:opt-un [::ip-addr ::props]))
|
||||||
|
|
||||||
|
(def ^:private backend-event-xform
|
||||||
(comp
|
(comp
|
||||||
(filter :profile-id)
|
(filter #(us/valid? ::backend-event %))
|
||||||
(map clean-props)))
|
(map clean-props)))
|
||||||
|
|
||||||
(defmethod ig/init-key ::collector
|
(defmethod ig/init-key ::collector
|
||||||
|
@ -166,34 +164,33 @@
|
||||||
(constantly nil))
|
(constantly nil))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [input (a/chan 512 event-xform)
|
(let [input (a/chan 512 backend-event-xform)
|
||||||
buffer (aa/batch input {:max-batch-size 100
|
buffer (aa/batch input {:max-batch-size 100
|
||||||
:max-batch-age (* 10 1000) ; 10s
|
:max-batch-age (* 10 1000) ; 10s
|
||||||
:init []})]
|
:init []})]
|
||||||
|
|
||||||
(l/info :hint "audit log collector initialized")
|
(l/info :hint "audit log collector initialized")
|
||||||
(a/go-loop []
|
(a/go-loop []
|
||||||
(when-let [[_type events] (a/<! buffer)]
|
(when-let [[_type events] (a/<! buffer)]
|
||||||
(let [res (a/<! (persist-events cfg events))]
|
(let [res (a/<! (persist-events cfg events))]
|
||||||
(when (ex/exception? res)
|
(when (ex/exception? res)
|
||||||
(l/error :hint "error on persisting events"
|
(l/error :hint "error on persisting events" :cause res))
|
||||||
:cause res)))
|
(recur))))
|
||||||
(recur)))
|
|
||||||
|
|
||||||
(fn [& {:keys [cmd] :as params}]
|
(fn [& {:keys [cmd] :as params}]
|
||||||
|
(case cmd
|
||||||
|
:stop
|
||||||
|
(a/close! input)
|
||||||
|
|
||||||
|
:submit
|
||||||
(let [params (-> params
|
(let [params (-> params
|
||||||
(dissoc :cmd)
|
(dissoc :cmd)
|
||||||
(assoc :tracked-at (dt/now)))]
|
(assoc :tracked-at (dt/now)))]
|
||||||
(case cmd
|
(when-not (a/offer! input params)
|
||||||
:stop (a/close! input)
|
(l/warn :hint "activity channel is full"))))))))
|
||||||
:submit (when-not (a/offer! input params)
|
|
||||||
(l/warn :msg "activity channel is full"))))))))
|
|
||||||
|
|
||||||
|
|
||||||
(defn- persist-events
|
(defn- persist-events
|
||||||
[{:keys [pool executor] :as cfg} events]
|
[{:keys [pool executor] :as cfg} events]
|
||||||
(letfn [(event->row [event]
|
(letfn [(event->row [event]
|
||||||
(when (:profile-id event)
|
|
||||||
[(uuid/next)
|
[(uuid/next)
|
||||||
(:name event)
|
(:name event)
|
||||||
(:type event)
|
(:type event)
|
||||||
|
@ -201,7 +198,7 @@
|
||||||
(:tracked-at event)
|
(:tracked-at event)
|
||||||
(some-> (:ip-addr event) db/inet)
|
(some-> (:ip-addr event) db/inet)
|
||||||
(db/tjson (:props event))
|
(db/tjson (:props event))
|
||||||
"backend"]))]
|
"backend"])]
|
||||||
(aa/with-thread executor
|
(aa/with-thread executor
|
||||||
(when (seq events)
|
(when (seq events)
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
|
|
|
@ -113,7 +113,6 @@
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:ip-addr (audit/parse-client-ip request)
|
:ip-addr (audit/parse-client-ip request)
|
||||||
:props props)))
|
:props props)))
|
||||||
|
|
||||||
result))
|
result))
|
||||||
mdata)))
|
mdata)))
|
||||||
|
|
||||||
|
|
|
@ -162,18 +162,17 @@
|
||||||
profile (->> (assoc params :is-active is-active)
|
profile (->> (assoc params :is-active is-active)
|
||||||
(create-profile conn)
|
(create-profile conn)
|
||||||
(create-profile-relations conn)
|
(create-profile-relations conn)
|
||||||
(decode-profile-row))]
|
(decode-profile-row))
|
||||||
|
|
||||||
|
invitation (when-let [token (:invitation-token params)]
|
||||||
|
(tokens :verify {:token token :iss :team-invitation}))]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
;; If invitation token comes in params, this is because the
|
;; If invitation token comes in params, this is because the user comes from team-invitation process;
|
||||||
;; user comes from team-invitation process; in this case,
|
;; in this case, regenerate token and send back to the user a new invitation token (and mark current
|
||||||
;; regenerate token and send back to the user a new invitation
|
;; session as logged). This happens only if the invitation email matches with the register email.
|
||||||
;; token (and mark current session as logged).
|
(and (some? invitation) (= (:email profile) (:member-email invitation)))
|
||||||
(some? (:invitation-token params))
|
(let [claims (assoc invitation :member-id (:id profile))
|
||||||
(let [token (:invitation-token params)
|
|
||||||
claims (tokens :verify {:token token :iss :team-invitation})
|
|
||||||
claims (assoc claims
|
|
||||||
:member-id (:id profile)
|
|
||||||
:member-email (:email profile))
|
|
||||||
token (tokens :generate claims)
|
token (tokens :generate claims)
|
||||||
resp {:invitation-token token}]
|
resp {:invitation-token token}]
|
||||||
(with-meta resp
|
(with-meta resp
|
||||||
|
@ -315,28 +314,22 @@
|
||||||
(validate-profile)
|
(validate-profile)
|
||||||
(profile/strip-private-attrs)
|
(profile/strip-private-attrs)
|
||||||
(profile/populate-additional-data conn)
|
(profile/populate-additional-data conn)
|
||||||
(decode-profile-row))]
|
(decode-profile-row))
|
||||||
(if-let [token (:invitation-token params)]
|
|
||||||
;; If the request comes with an invitation token, this means
|
|
||||||
;; that user wants to accept it with different user. A very
|
|
||||||
;; strange case but still can happen. In this case, we
|
|
||||||
;; proceed in the same way as in register: regenerate the
|
|
||||||
;; invitation token and return it to the user for proper
|
|
||||||
;; invitation acceptation.
|
|
||||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
|
||||||
claims (assoc claims
|
|
||||||
:member-id (:id profile)
|
|
||||||
:member-email (:email profile))
|
|
||||||
token (tokens :generate claims)]
|
|
||||||
(with-meta {:invitation-token token}
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
::audit/props (audit/profile->props profile)
|
|
||||||
::audit/profile-id (:id profile)}))
|
|
||||||
|
|
||||||
(with-meta profile
|
invitation (when-let [token (:invitation-token params)]
|
||||||
|
(tokens :verify {: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 loging with other email and
|
||||||
|
;; accept invitation with other email
|
||||||
|
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
|
||||||
|
{:invitation-token (:invitation-token params)}
|
||||||
|
profile)]
|
||||||
|
|
||||||
|
(with-meta response
|
||||||
{:transform-response ((:create session) (:id profile))
|
{:transform-response ((:create session) (:id profile))
|
||||||
::audit/props (audit/profile->props profile)
|
::audit/props (audit/profile->props profile)
|
||||||
::audit/profile-id (:id profile)}))))))
|
::audit/profile-id (:id profile)})))))
|
||||||
|
|
||||||
;; --- MUTATION: Logout
|
;; --- MUTATION: Logout
|
||||||
|
|
||||||
|
|
|
@ -387,8 +387,7 @@
|
||||||
:code :member-is-muted
|
:code :member-is-muted
|
||||||
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces"))
|
||||||
|
|
||||||
;; Secondly check if the invited member email is part of the
|
;; Secondly check if the invited member email is part of the global spam/bounce report.
|
||||||
;; global spam/bounce report.
|
|
||||||
(when (eml/has-bounce-reports? conn email)
|
(when (eml/has-bounce-reports? conn email)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :email-has-permanent-bounces
|
:code :email-has-permanent-bounces
|
||||||
|
@ -415,13 +414,21 @@
|
||||||
(s/and ::create-team (s/keys :req-un [::emails ::role])))
|
(s/and ::create-team (s/keys :req-un [::emails ::role])))
|
||||||
|
|
||||||
(sv/defmethod ::create-team-and-invite-members
|
(sv/defmethod ::create-team-and-invite-members
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id emails role] :as params}]
|
[{:keys [pool audit] :as cfg} {:keys [profile-id emails role] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [team (create-team conn params)
|
(let [team (create-team conn params)
|
||||||
profile (db/get-by-id conn :profile profile-id)]
|
profile (db/get-by-id conn :profile profile-id)]
|
||||||
|
|
||||||
;; Create invitations for all provided emails.
|
;; Create invitations for all provided emails.
|
||||||
(doseq [email emails]
|
(doseq [email emails]
|
||||||
|
(audit :cmd :submit
|
||||||
|
:type "mutation"
|
||||||
|
:name "create-team-invitation"
|
||||||
|
:profile-id profile-id
|
||||||
|
:props {:email email
|
||||||
|
:role role
|
||||||
|
:profile-id profile-id})
|
||||||
|
|
||||||
(create-team-invitation
|
(create-team-invitation
|
||||||
(assoc cfg
|
(assoc cfg
|
||||||
:conn conn
|
:conn conn
|
||||||
|
|
|
@ -131,77 +131,39 @@
|
||||||
|
|
||||||
|
|
||||||
(defmethod process-token :team-invitation
|
(defmethod process-token :team-invitation
|
||||||
[{:keys [session] :as cfg} {:keys [profile-id token]} {:keys [member-id] :as claims}]
|
[cfg {:keys [profile-id token]} {:keys [member-id] :as claims}]
|
||||||
(us/assert ::team-invitation-claims claims)
|
(us/assert ::team-invitation-claims claims)
|
||||||
(cond
|
(cond
|
||||||
;; This happens when token is filled with member-id and current
|
;; This happens when token is filled with member-id and current
|
||||||
;; user is already logged in with some account.
|
;; user is already logged in with exactly invited account.
|
||||||
(and (uuid? profile-id)
|
(and (uuid? profile-id) (uuid? member-id) (= member-id profile-id))
|
||||||
(uuid? member-id))
|
|
||||||
(let [profile (accept-invitation cfg claims)]
|
(let [profile (accept-invitation cfg claims)]
|
||||||
(if (= member-id profile-id)
|
|
||||||
;; If the current session is already matches the invited
|
|
||||||
;; member, then just return the token and leave the frontend
|
|
||||||
;; app redirect to correct team.
|
|
||||||
(assoc claims :state :created)
|
|
||||||
|
|
||||||
;; If the session does not matches the invited member, replace
|
|
||||||
;; the session with a new one matching the invited member.
|
|
||||||
;; This technique should be considered secure because the
|
|
||||||
;; user clicking the link he already has access to the email
|
|
||||||
;; account.
|
|
||||||
(with-meta
|
|
||||||
(assoc claims :state :created)
|
|
||||||
{:transform-response ((:create session) member-id)
|
|
||||||
::audit/name "accept-team-invitation"
|
|
||||||
::audit/props (merge
|
|
||||||
(audit/profile->props profile)
|
|
||||||
{:team-id (:team-id claims)
|
|
||||||
:role (:role claims)})
|
|
||||||
::audit/profile-id profile-id})))
|
|
||||||
|
|
||||||
;; This happens when member-id is not filled in the invitation but
|
|
||||||
;; the user already has an account (probably with other mail) and
|
|
||||||
;; is already logged-in.
|
|
||||||
(and (uuid? profile-id)
|
|
||||||
(nil? member-id))
|
|
||||||
(let [profile (accept-invitation cfg (assoc claims :member-id profile-id))]
|
|
||||||
(with-meta
|
(with-meta
|
||||||
(assoc claims :state :created)
|
(assoc claims :state :created)
|
||||||
{::audit/name "accept-team-invitation"
|
{::audit/name "accept-team-invitation"
|
||||||
::audit/props (merge
|
|
||||||
(audit/profile->props profile)
|
|
||||||
{:team-id (:team-id claims)
|
|
||||||
:role (:role claims)})
|
|
||||||
::audit/profile-id profile-id}))
|
|
||||||
|
|
||||||
;; This happens when member-id is filled but the accessing user is
|
|
||||||
;; not logged-in. In this case we proceed to accept invitation and
|
|
||||||
;; leave the user logged-in.
|
|
||||||
(and (nil? profile-id)
|
|
||||||
(uuid? member-id))
|
|
||||||
(let [profile (accept-invitation cfg claims)]
|
|
||||||
(with-meta
|
|
||||||
(assoc claims :state :created)
|
|
||||||
{:transform-response ((:create session) member-id)
|
|
||||||
::audit/name "accept-team-invitation"
|
|
||||||
::audit/props (merge
|
::audit/props (merge
|
||||||
(audit/profile->props profile)
|
(audit/profile->props profile)
|
||||||
{:team-id (:team-id claims)
|
{:team-id (:team-id claims)
|
||||||
:role (:role claims)})
|
:role (:role claims)})
|
||||||
::audit/profile-id member-id}))
|
::audit/profile-id member-id}))
|
||||||
|
|
||||||
;; In this case, we wait until frontend app redirect user to
|
;; This case means that invitation token does not match with
|
||||||
;; registration page, the user is correctly registered and the
|
;; registred user, so we need to indicate to frontend to redirect
|
||||||
;; register mutation call us again with the same token to finally
|
;; it to register page.
|
||||||
;; create the corresponding team-profile relation from the first
|
(nil? member-id)
|
||||||
;; condition of this if.
|
{:invitation-token token
|
||||||
|
:iss :team-invitation
|
||||||
|
:redirect-to :auth-register
|
||||||
|
:state :pending}
|
||||||
|
|
||||||
|
;; In all other cases, just tell to fontend to redirect the user
|
||||||
|
;; to the login page.
|
||||||
:else
|
:else
|
||||||
{:invitation-token token
|
{:invitation-token token
|
||||||
:iss :team-invitation
|
:iss :team-invitation
|
||||||
|
:redirect-to :auth-login
|
||||||
:state :pending}))
|
:state :pending}))
|
||||||
|
|
||||||
|
|
||||||
;; --- Default
|
;; --- Default
|
||||||
|
|
||||||
(defmethod process-token :default
|
(defmethod process-token :default
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.tokens
|
(ns app.tokens
|
||||||
"Tokens generation service."
|
"Tokens generation service."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
|
|
||||||
(defn- generate
|
(defn- generate
|
||||||
[cfg claims]
|
[cfg claims]
|
||||||
(let [payload (t/encode claims)]
|
(let [payload (-> claims d/without-nils t/encode)]
|
||||||
(jwe/encrypt payload (::secret cfg) {:alg :a256kw :enc :a256gcm})))
|
(jwe/encrypt payload (::secret cfg) {:alg :a256kw :enc :a256gcm})))
|
||||||
|
|
||||||
(defn- verify
|
(defn- verify
|
||||||
|
|
|
@ -41,13 +41,15 @@
|
||||||
[tdata]
|
[tdata]
|
||||||
(case (:state tdata)
|
(case (:state tdata)
|
||||||
:created
|
:created
|
||||||
(st/emit! (dm/success (tr "auth.notifications.team-invitation-accepted"))
|
(st/emit!
|
||||||
|
(dm/success (tr "auth.notifications.team-invitation-accepted"))
|
||||||
(du/fetch-profile)
|
(du/fetch-profile)
|
||||||
(rt/nav :dashboard-projects {:team-id (:team-id tdata)}))
|
(rt/nav :dashboard-projects {:team-id (:team-id tdata)}))
|
||||||
|
|
||||||
:pending
|
:pending
|
||||||
(let [token (:invitation-token tdata)]
|
(let [token (:invitation-token tdata)
|
||||||
(st/emit! (rt/nav :auth-register {} {:invitation-token token})))))
|
route-id (:redirect-to tdata :auth-register)]
|
||||||
|
(st/emit! (rt/nav route-id {} {:invitation-token token})))))
|
||||||
|
|
||||||
(defmethod handle-token :default
|
(defmethod handle-token :default
|
||||||
[_tdata]
|
[_tdata]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue