penpot/backend/src/app/rpc/commands/verify_token.clj
Andrey Antukh bbe0b22a8b 🐛 Fix unhandled exception on accepting invitation
In an interaction with the audit log; happens when an old invitation
(with created-by as nil) is accepted.
2024-12-13 11:48:37 +01:00

212 lines
7.5 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.commands.verify-token
(:require
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.types.team :as types.team]
[app.config :as cf]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.tokens.spec.team-invitation :as-alias spec.team-invitation]
[app.util.services :as sv]
[app.util.time :as dt]))
(defmulti process-token (fn [_ _ claims] (:iss claims)))
(def ^:private schema:verify-token
[:map {:title "verify-token"}
[:token [:string {:max 5000}]]])
(sv/defmethod ::verify-token
{::rpc/auth false
::doc/added "1.15"
::doc/module :auth
::sm/params schema:verify-token}
[cfg {:keys [token] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token})]
(db/tx-run! cfg process-token params claims)))
(defmethod process-token :change-email
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id email] :as claims}]
(let [email (profile/clean-email email)]
(when (profile/get-profile-by-email conn email)
(ex/raise :type :validation
:code :email-already-exists))
(db/update! conn :profile
{:email email}
{:id profile-id})
(rph/with-meta claims
{::audit/name "update-profile-email"
::audit/props {:email email}
::audit/profile-id profile-id})))
(defmethod process-token :verify-email
[{:keys [::db/conn] :as cfg} _ {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)
claims (assoc claims :profile profile)]
(when-not (:is-active profile)
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)}))
(-> claims
(rph/with-transform (session/create-fn cfg profile-id))
(rph/with-meta {::audit/name "verify-profile-email"
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))
(defmethod process-token :auth
[{:keys [::db/conn] :as cfg} _params {:keys [profile-id] :as claims}]
(let [profile (profile/get-profile conn profile-id)]
(assoc claims :profile profile)))
;; --- Team Invitation
(defn- accept-invitation
[{:keys [::db/conn] :as cfg} {:keys [team-id role member-email] :as claims} invitation member]
(let [;; Update the role if there is an invitation
role (or (some-> invitation :role keyword) role)
params (merge
{:team-id team-id
:profile-id (:id member)}
(get types.team/permissions-for-role role))]
;; Do not allow blocked users accept invitations.
(when (:is-blocked member)
(ex/raise :type :restriction
:code :profile-blocked))
(quotes/check! cfg {::quotes/id ::quotes/profiles-per-team
::quotes/profile-id (:id member)
::quotes/team-id team-id})
;; Insert the invited member to the team
(db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true})
;; If profile is not yet verified, mark it as verified because
;; accepting an invitation link serves as verification.
(when-not (:is-active member)
(db/update! conn :profile
{:is-active true}
{:id (:id member)}))
;; Delete the invitation
(db/delete! conn :team-invitation
{:team-id team-id :email-to member-email})
;; Delete any request
(db/delete! conn :team-access-request
{:team-id team-id :requester-id (:id member)})
(assoc member :is-active true)))
(def schema:team-invitation-claims
[:map {:title "TeamInvitationClaims"}
[:iss :keyword]
[:exp ::dt/instant]
[:profile-id ::sm/uuid]
[:role ::types.team/role]
[:team-id ::sm/uuid]
[:member-email ::sm/email]
[:member-id {:optional true} ::sm/uuid]])
(def valid-team-invitation-claims?
(sm/lazy-validator schema:team-invitation-claims))
(defmethod process-token :team-invitation
[{:keys [::db/conn] :as cfg}
{:keys [::rpc/profile-id token] :as params}
{:keys [member-id team-id member-email] :as claims}]
(when-not (valid-team-invitation-claims? claims)
(ex/raise :type :validation
:code :invalid-invitation-token
:hint "invitation token contains unexpected data"))
(let [invitation (db/get* conn :team-invitation
{:team-id team-id :email-to member-email})
profile (db/get* conn :profile
{:id profile-id}
{:columns [:id :email]})
registration-disabled? (not (contains? cf/flags :registration))]
(when (nil? invitation)
(ex/raise :type :validation
:code :invalid-token
:hint "no invitation associated with the token"))
(if (some? profile)
(if (or (= member-id profile-id)
(= member-email (:email profile)))
;; if we have logged-in user and it matches the invitation we proceed
;; with accepting the invitation and joining the current profile to the
;; invited team.
(let [props {:team-id (:team-id claims)
:role (:role claims)
:invitation-id (:id invitation)}]
(audit/submit!
cfg
(-> (audit/event-from-rpc-params params)
(assoc ::audit/name "accept-team-invitation")
(assoc ::audit/props props)))
;; NOTE: Backward compatibility; old invitations can
;; have the `created-by` to be nil; so in this case we
;; don't submit this event to the audit-log
(when-let [created-by (:created-by invitation)]
(audit/submit!
cfg
(-> (audit/event-from-rpc-params params)
(assoc ::audit/profile-id created-by)
(assoc ::audit/name "accept-team-invitation-from")
(assoc ::audit/props (assoc props
:profile-id (:id profile)
:email (:email profile))))))
(accept-invitation cfg claims invitation profile)
(assoc claims :state :created))
(ex/raise :type :validation
:code :invalid-token
:hint "logged-in user does not matches the invitation"))
;; If we have not logged-in user, and invitation comes with member-id we
;; redirect user to login, if no memeber-id is present and in the invitation
;; token and registration is enabled, we redirect user the the register page.
{:invitation-token token
:iss :team-invitation
:redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register)
:state :pending})))
;; --- Default
(defmethod process-token :default
[_ _ _]
(ex/raise :type :validation
:code :invalid-token))