Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2022-02-21 16:29:45 +01:00
commit e13bceeb59
7 changed files with 117 additions and 156 deletions

View file

@ -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]

View file

@ -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)))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]