♻️ Add admin facilities on the code base

- Fix bugs related to orphan teams on profile deletion
- Separate session based profile-id param from api user provided
This commit is contained in:
Andrey Antukh 2022-12-21 11:53:56 +01:00
parent 53d9b547c3
commit b929564fa7
53 changed files with 872 additions and 468 deletions

View file

@ -6,6 +6,7 @@
(ns app.rpc.commands.auth
(:require
[app.auth :as auth]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.spec :as us]
@ -15,6 +16,8 @@
[app.emails :as eml]
[app.http.session :as session]
[app.loggers.audit :as audit]
[app.main :as-alias main]
[app.rpc :as-alias rpc]
[app.rpc.climit :as climit]
[app.rpc.commands.teams :as teams]
[app.rpc.doc :as-alias doc]
@ -23,7 +26,6 @@
[app.tokens :as tokens]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
@ -31,7 +33,6 @@
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/string)
(s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string)
(s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string)
@ -40,22 +41,6 @@
;; ---- HELPERS
(defn derive-password
[password]
(hashers/derive password
{:alg :argon2id
:memory 16384
:iterations 20
:parallelism 2}))
(defn verify-password
[attempt password]
(try
(hashers/verify attempt password)
(catch Exception _e
{:update false
:valid false})))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string."
@ -84,7 +69,7 @@
;; ---- COMMAND: login with password
(defn login-with-password
[{:keys [pool session sprops] :as cfg} {:keys [email password] :as params}]
[{:keys [::db/pool session] :as cfg} {:keys [email password scope] :as params}]
(when-not (contains? cf/flags :login)
(ex/raise :type :restriction
@ -96,7 +81,7 @@
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password"))
(:valid (verify-password password (:password profile))))
(:valid (auth/verify-password password (:password profile))))
(validate-profile [profile]
(when-not profile
@ -126,27 +111,37 @@
(profile/decode-profile-row))
invitation (when-let [token (:invitation-token params)]
(tokens/verify sprops {:token token :iss :team-invitation}))
(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)}
profile)]
(update profile :is-admin (fn [admin?]
(or admin?
(let [admins (cf/get :admins)]
(contains? admins (:email profile)))))))]
(when (and (nil? (:default-team-id profile))
(not= scope "admin"))
(ex/raise :type :restriction
:code :admin-only-profile
:hint "can't login with admin-only profile"))
(-> response
(rph/with-transform (session/create-fn session (:id profile)))
(rph/with-meta {::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(s/def ::scope ::us/string)
(s/def ::login-with-password
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
:opt-un [::invitation-token ::scope]))
(sv/defmethod ::login-with-password
"Performs authentication using penpot password."
{:auth false
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
[cfg params]
@ -155,11 +150,11 @@
;; ---- COMMAND: Logout
(s/def ::logout
(s/keys :opt-un [::profile-id]))
(s/keys :opt [::rpc/profile-id]))
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
{:auth false
{::rpc/auth false
::doc/added "1.15"}
[{:keys [session] :as cfg} _]
(rph/with-transform {} (session/delete-fn session)))
@ -167,13 +162,13 @@
;; ---- COMMAND: Recover Profile
(defn recover-profile
[{:keys [pool sprops] :as cfg} {:keys [token password]}]
[{:keys [::db/pool] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token]
(let [tdata (tokens/verify sprops {:token token :iss :password-recovery})]
(let [tdata (tokens/verify (::main/props cfg) {:token token :iss :password-recovery})]
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (derive-password password)]
(let [pwd (auth/derive-password password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool]
@ -186,7 +181,7 @@
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile
{:auth false
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
[cfg params]
@ -195,13 +190,13 @@
;; ---- COMMAND: Prepare Register
(defn validate-register-attempt!
[{:keys [pool sprops]} params]
[{:keys [::db/pool] :as cfg} params]
(when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)
(let [invitation (tokens/verify sprops {:token (:invitation-token params) :iss :team-invitation})]
(let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
@ -235,7 +230,7 @@
(pos? (compare elapsed register-retry-threshold))))
(defn prepare-register
[{:keys [pool sprops] :as cfg} params]
[{:keys [::db/pool] :as cfg} params]
(validate-register-attempt! cfg params)
@ -264,7 +259,7 @@
params (d/without-nils params)
token (tokens/generate sprops params)]
token (tokens/generate (::main/props cfg) params)]
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
@ -273,7 +268,7 @@
:opt-un [::invitation-token]))
(sv/defmethod ::prepare-register-profile
{:auth false
{::rpc/auth false
::doc/added "1.15"}
[cfg params]
(prepare-register cfg params))
@ -293,7 +288,7 @@
(db/tjson))
password (if-let [password (:password params)]
(derive-password password)
(auth/derive-password password)
"!")
locale (:locale params)
@ -339,15 +334,15 @@
(assoc :default-project-id (:default-project-id team)))))
(defn send-email-verification!
[conn sprops profile]
(let [vtoken (tokens/generate sprops
[conn props profile]
(let [vtoken (tokens/generate props
{:iss :verify-email
:exp (dt/in-future "72h")
:profile-id (:id profile)
:email (:email profile)})
;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook
ptoken (tokens/generate sprops
ptoken (tokens/generate props
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
@ -360,8 +355,8 @@
:extra-data ptoken})))
(defn register-profile
[{:keys [conn sprops session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify sprops {:token token :iss :prepared-register})
[{:keys [conn session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register})
params (merge params claims)
is-active (or (:is-active params)
@ -377,7 +372,7 @@
(create-profile-relations conn)
(profile/decode-profile-row)))
invitation (when-let [token (:invitation-token params)]
(tokens/verify sprops {:token token :iss :team-invitation}))]
(tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))]
;; If profile is filled in claims, means it tries to register
;; again, so we proceed to update the modified-at attr
@ -399,7 +394,7 @@
;; email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens/generate sprops claims)
token (tokens/generate (::main/props cfg) claims)
resp {:invitation-token token}]
(-> resp
(rph/with-transform (session/create-fn session (:id profile)))
@ -426,7 +421,7 @@
;; In all other cases, send a verification email.
:else
(do
(send-email-verification! conn sprops profile)
(send-email-verification! conn (::main/props cfg) profile)
(rph/with-meta profile
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
@ -435,10 +430,10 @@
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile
{:auth false
{::rpc/auth false
::climit/queue :auth
::doc/added "1.15"}
[{:keys [pool] :as cfg} params]
[{:keys [::db/pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(register-profile params))))
@ -446,16 +441,16 @@
;; ---- COMMAND: Request Profile Recovery
(defn request-profile-recovery
[{:keys [pool sprops] :as cfg} {:keys [email] :as params}]
[{:keys [::db/pool] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate sprops
(let [token (tokens/generate (::main/props cfg)
{:iss :password-recovery
:exp (dt/in-future "15m")
:profile-id id})]
(assoc profile :token token)))
(send-email-notification [conn profile]
(let [ptoken (tokens/generate sprops
(let [ptoken (tokens/generate (::main/props cfg)
{:iss :profile-identity
:profile-id (:id profile)
:exp (dt/in-future {:days 30})})]
@ -493,7 +488,7 @@
(s/keys :req-un [::email]))
(sv/defmethod ::request-profile-recovery
{:auth false
{::rpc/auth false
::doc/added "1.15"}
[cfg params]
(request-profile-recovery cfg params))