Merge pull request #7432 from penpot/niwinz-develop-virtual-clock

🎉 Add virtual clock support
This commit is contained in:
Alejandro Alonso
2025-10-07 12:27:49 +02:00
committed by GitHub
23 changed files with 235 additions and 148 deletions

View File

@@ -45,7 +45,41 @@ Debug Main Page
</form> </form>
</fieldset> </fieldset>
<fieldset>
<legend>VIRTUAL CLOCK</legend>
<desc>
<p>
CURRENT CLOCK: <b>{{current-clock}}</b>
<br />
CURRENT OFFSET: <b>{{current-offset}}</b>
<br />
CURRENT TIME: <b>{{current-time}}</b>
</p>
<p>Examples: 3h, -7h, 24h (allowed suffixes: h, s)</p>
</desc>
<form method="post" action="/dbg/actions/set-virtual-clock">
<div class="row">
<input type="text" name="offset" placeholder="3h" value="" />
</div>
<div class="row">
<label for="force-verify">Are you sure?</label>
<input id="force-verify" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" name="submit" value="Submit" />
<input type="submit" name="reset" value="Reset" />
</div>
</form>
</fieldset>
</section> </section>

View File

@@ -434,10 +434,10 @@
(sm/validator schema:info)) (sm/validator schema:info))
(defn- get-info (defn- get-info
[{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}] [{:keys [::provider] :as cfg} {:keys [params] :as request}]
(let [state (get params :state) (let [state (get params :state)
code (get params :code) code (get params :code)
state (tokens/verify props {:token state :iss :oauth}) state (tokens/verify cfg {:token state :iss :oauth})
tdata (fetch-access-token cfg code) tdata (fetch-access-token cfg code)
info (case (cf/get :oidc-user-info-source) info (case (cf/get :oidc-user-info-source)
:token (get-user-info cfg tdata) :token (get-user-info cfg tdata)
@@ -516,7 +516,7 @@
:iss :prepared-register :iss :prepared-register
:exp (ct/in-future {:hours 48})) :exp (ct/in-future {:hours 48}))
params {:token (tokens/generate (::setup/props cfg) info) params {:token (tokens/generate cfg info)
:provider (:provider (:path-params request)) :provider (:provider (:path-params request))
:fullname (:fullname info)} :fullname (:fullname info)}
params (d/without-nils params)] params (d/without-nils params)]
@@ -569,7 +569,7 @@
:else :else
(let [sxf (session/create-fn cfg (:id profile)) (let [sxf (session/create-fn cfg (:id profile))
token (or (:invitation-token info) token (or (:invitation-token info)
(tokens/generate (::setup/props cfg) (tokens/generate cfg
{:iss :auth {:iss :auth
:exp (ct/in-future "15m") :exp (ct/in-future "15m")
:profile-id (:id profile)})) :profile-id (:id profile)}))
@@ -620,8 +620,7 @@
:external-session-id esid :external-session-id esid
:props props :props props
:exp (ct/in-future "4h")} :exp (ct/in-future "4h")}
state (tokens/generate (::setup/props cfg) state (tokens/generate cfg (d/without-nils params))
(d/without-nils params))
uri (build-auth-uri cfg state)] uri (build-auth-uri cfg state)]
{::yres/status 200 {::yres/status 200
::yres/body {:redirect-uri uri}})) ::yres/body {:redirect-uri uri}}))

View File

@@ -23,9 +23,9 @@
(second))) (second)))
(defn- decode-token (defn- decode-token
[props token] [cfg token]
(when token (when token
(tokens/verify props {:token token :iss "access-token"}))) (tokens/verify cfg {:token token :iss "access-token"})))
(def sql:get-token-data (def sql:get-token-data
"SELECT perms, profile_id, expires_at "SELECT perms, profile_id, expires_at
@@ -43,11 +43,11 @@
(defn- wrap-soft-auth (defn- wrap-soft-auth
"Soft Authentication, will be executed synchronously on the undertow "Soft Authentication, will be executed synchronously on the undertow
worker thread." worker thread."
[handler {:keys [::setup/props]}] [handler cfg]
(letfn [(handle-request [request] (letfn [(handle-request [request]
(try (try
(let [token (get-token request) (let [token (get-token request)
claims (decode-token props token)] claims (decode-token cfg token)]
(cond-> request (cond-> request
(map? claims) (map? claims)
(assoc ::id (:tid claims)))) (assoc ::id (:tid claims))))

View File

@@ -107,7 +107,7 @@
[cfg headers] [cfg headers]
(let [tdata (get headers "x-penpot-data")] (let [tdata (get headers "x-penpot-data")]
(when-not (str/empty? tdata) (when-not (str/empty? tdata)
(let [result (tokens/verify (::setup/props cfg) {:token tdata :iss :profile-identity})] (let [result (tokens/verify cfg {:token tdata :iss :profile-identity})]
(:profile-id result))))) (:profile-id result)))))
(defn- parse-notification (defn- parse-notification

View File

@@ -27,6 +27,7 @@
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.setup.clock :as clock]
[app.srepl.main :as srepl] [app.srepl.main :as srepl]
[app.storage :as-alias sto] [app.storage :as-alias sto]
[app.storage.tmp :as tmp] [app.storage.tmp :as tmp]
@@ -49,11 +50,17 @@
(defn index-handler (defn index-handler
[_cfg _request] [_cfg _request]
(let [{:keys [clock offset]} @clock/current]
{::yres/status 200 {::yres/status 200
::yres/headers {"content-type" "text/html"} ::yres/headers {"content-type" "text/html"}
::yres/body (-> (io/resource "app/templates/debug.tmpl") ::yres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {:version (:full cf/version) (tmpl/render {:version (:full cf/version)
:supported-features cfeat/supported-features}))}) :current-clock (str clock)
:current-offset (if offset
(ct/format-duration offset)
"NO OFFSET")
:current-time (ct/format-inst (ct/now) :http)
:supported-features cfeat/supported-features}))}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FILE CHANGES ;; FILE CHANGES
@@ -417,7 +424,6 @@
::yres/headers {"content-type" "text/plain"} ::yres/headers {"content-type" "text/plain"}
::yres/body "OK"})) ::yres/body "OK"}))
(defn- handle-team-features (defn- handle-team-features
[cfg {:keys [params] :as request}] [cfg {:keys [params] :as request}]
(let [team-id (some-> params :team-id d/parse-uuid) (let [team-id (some-> params :team-id d/parse-uuid)
@@ -462,6 +468,20 @@
::yres/headers {"content-type" "text/plain"} ::yres/headers {"content-type" "text/plain"}
::yres/body "OK"})))) ::yres/body "OK"}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; VIRTUAL CLOCK
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- set-virtual-clock
[_ {:keys [params] :as request}]
(let [offset (some-> params :offset str/trim not-empty ct/duration)
reset? (contains? params :reset)]
(if (or reset? (zero? (inst-ms offset)))
(clock/set-offset! nil)
(clock/set-offset! offset))
{::yres/status 302
::yres/headers {"location" "/dbg"}}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OTHER SMALL VIEWS/HANDLERS ;; OTHER SMALL VIEWS/HANDLERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -548,6 +568,8 @@
["/error/:id" {:handler (partial error-handler cfg)}] ["/error/:id" {:handler (partial error-handler cfg)}]
["/error" {:handler (partial error-list-handler cfg)}] ["/error" {:handler (partial error-list-handler cfg)}]
["/actions" {:middleware [[errors]]} ["/actions" {:middleware [[errors]]}
["/set-virtual-clock"
{:handler (partial set-virtual-clock cfg)}]
["/resend-email-verification" ["/resend-email-verification"
{:handler (partial resend-email-notification cfg)}] {:handler (partial resend-email-notification cfg)}]
["/reset-file-version" ["/reset-file-version"

View File

@@ -79,8 +79,7 @@
(defn- authenticate (defn- authenticate
[cfg request] [cfg request]
(let [token (-> request :params :token) (let [token (-> request :params :token)
props (get cfg ::setup/props) result (tokens/verify cfg {:token token :iss "authentication"})]
result (tokens/verify props {:token token :iss "authentication"})]
{::yres/status 200 {::yres/status 200
::yres/body result})) ::yres/body result}))

View File

@@ -72,7 +72,7 @@
(sm/validator schema:params)) (sm/validator schema:params))
(defn- prepare-session-params (defn- prepare-session-params
[key params] [params key]
(assert (string? key) "expected key to be a string") (assert (string? key) "expected key to be a string")
(assert (not (str/blank? key)) "expected key to be not empty") (assert (not (str/blank? key)) "expected key to be not empty")
(assert (valid-params? params) "expected valid params") (assert (valid-params? params) "expected valid params")
@@ -90,7 +90,9 @@
(db/exec-one! pool (sql/select :http-session {:id token}))) (db/exec-one! pool (sql/select :http-session {:id token})))
(write! [_ key params] (write! [_ key params]
(let [params (prepare-session-params key params)] (let [params (-> params
(assoc :created-at (ct/now))
(prepare-session-params key))]
(db/insert! pool :http-session params) (db/insert! pool :http-session params)
params)) params))
@@ -113,7 +115,9 @@
(get @cache token)) (get @cache token))
(write! [_ key params] (write! [_ key params]
(let [params (prepare-session-params key params)] (let [params (-> params
(assoc :created-at (ct/now))
(prepare-session-params key))]
(swap! cache assoc key params) (swap! cache assoc key params)
params)) params))
@@ -150,16 +154,15 @@
(declare ^:private gen-token) (declare ^:private gen-token)
(defn create-fn (defn create-fn
[{:keys [::manager ::setup/props]} profile-id] [{:keys [::manager] :as cfg} profile-id]
(assert (manager? manager) "expected valid session manager") (assert (manager? manager) "expected valid session manager")
(assert (uuid? profile-id) "expected valid uuid for profile-id") (assert (uuid? profile-id) "expected valid uuid for profile-id")
(fn [request response] (fn [request response]
(let [uagent (yreq/get-header request "user-agent") (let [uagent (yreq/get-header request "user-agent")
params {:profile-id profile-id params {:profile-id profile-id
:user-agent uagent :user-agent uagent}
:created-at (ct/now)} token (gen-token cfg params)
token (gen-token props params)
session (write! manager token params)] session (write! manager token params)]
(l/trace :hint "create" :profile-id (str profile-id)) (l/trace :hint "create" :profile-id (str profile-id))
(-> response (-> response
@@ -181,14 +184,14 @@
(clear-auth-data-cookie))))) (clear-auth-data-cookie)))))
(defn- gen-token (defn- gen-token
[props {:keys [profile-id created-at]}] [cfg {:keys [profile-id created-at]}]
(tokens/generate props {:iss "authentication" (tokens/generate cfg {:iss "authentication"
:iat created-at :iat created-at
:uid profile-id})) :uid profile-id}))
(defn- decode-token (defn- decode-token
[props token] [cfg token]
(when token (when token
(tokens/verify props {:token token :iss "authentication"}))) (tokens/verify cfg {:token token :iss "authentication"})))
(defn- get-token (defn- get-token
[request] [request]
@@ -208,12 +211,12 @@
(neg? (compare default-renewal-max-age elapsed))))) (neg? (compare default-renewal-max-age elapsed)))))
(defn- wrap-soft-auth (defn- wrap-soft-auth
[handler {:keys [::manager ::setup/props]}] [handler {:keys [::manager] :as cfg}]
(assert (manager? manager) "expected valid session manager") (assert (manager? manager) "expected valid session manager")
(letfn [(handle-request [request] (letfn [(handle-request [request]
(try (try
(let [token (get-token request) (let [token (get-token request)
claims (decode-token props token)] claims (decode-token cfg token)]
(cond-> request (cond-> request
(map? claims) (map? claims)
(-> (assoc ::token-claims claims) (-> (assoc ::token-claims claims)
@@ -256,7 +259,7 @@
(defn- assign-auth-token-cookie (defn- assign-auth-token-cookie
[response {token :id updated-at :updated-at}] [response {token :id updated-at :updated-at}]
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age) (let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
created-at (or updated-at (ct/now)) created-at updated-at
renewal (ct/plus created-at default-renewal-max-age) renewal (ct/plus created-at default-renewal-max-age)
expires (ct/plus created-at max-age) expires (ct/plus created-at max-age)
secure? (contains? cf/flags :secure-session-cookies) secure? (contains? cf/flags :secure-session-cookies)
@@ -279,7 +282,7 @@
domain (cf/get :auth-data-cookie-domain) domain (cf/get :auth-data-cookie-domain)
cname default-auth-data-cookie-name cname default-auth-data-cookie-name
created-at (or updated-at (ct/now)) created-at updated-at
renewal (ct/plus created-at default-renewal-max-age) renewal (ct/plus created-at default-renewal-max-age)
expires (ct/plus created-at max-age) expires (ct/plus created-at max-age)
@@ -313,13 +316,10 @@
(string? domain) (string? domain)
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0})))) (update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK: SESSION GC ;; TASK: SESSION GC
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; FIXME: MOVE
(defmethod ig/assert-key ::tasks/gc (defmethod ig/assert-key ::tasks/gc
[_ params] [_ params]
(assert (db/pool? (::db/pool params)) "expected valid database pool") (assert (db/pool? (::db/pool params)) "expected valid database pool")
@@ -332,16 +332,16 @@
(def ^:private (def ^:private
sql:delete-expired sql:delete-expired
"delete from http_session "DELETE FROM http_session
where updated_at < now() - ?::interval WHERE updated_at < ?::timestamptz
or (updated_at is null and or (updated_at is null and
created_at < now() - ?::interval)") created_at < ?::timestamptz)")
(defn- collect-expired-tasks (defn- collect-expired-tasks
[{:keys [::db/conn ::tasks/max-age]}] [{:keys [::db/conn ::tasks/max-age]}]
(let [interval (db/interval max-age) (let [threshold (ct/minus (ct/now) max-age)
result (db/exec-one! conn [sql:delete-expired interval interval]) result (-> (db/exec-one! conn [sql:delete-expired threshold threshold])
result (:next.jdbc/update-count result)] (db/get-update-count))]
(l/debug :task "gc" (l/debug :task "gc"
:hint "clean http sessions" :hint "clean http sessions"
:deleted result) :deleted result)
@@ -350,4 +350,5 @@
(defmethod ig/init-key ::tasks/gc (defmethod ig/init-key ::tasks/gc
[_ {:keys [::tasks/max-age] :as cfg}] [_ {:keys [::tasks/max-age] :as cfg}]
(l/debug :hint "initializing session gc task" :max-age max-age) (l/debug :hint "initializing session gc task" :max-age max-age)
(fn [_] (db/tx-run! cfg collect-expired-tasks))) (fn [_]
(db/tx-run! cfg collect-expired-tasks)))

View File

@@ -9,7 +9,6 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct]
[app.common.transit :as t] [app.common.transit :as t]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
@@ -53,9 +52,8 @@
(defn- send! (defn- send!
[{:keys [::uri] :as cfg} events] [{:keys [::uri] :as cfg} events]
(let [token (tokens/generate (::setup/props cfg) (let [token (tokens/generate cfg
{:iss "authentication" {:iss "authentication"
:iat (ct/now)
:uid uuid/zero}) :uid uuid/zero})
body (t/encode {:events events}) body (t/encode {:events events})
headers {"content-type" "application/transit+json" headers {"content-type" "application/transit+json"

View File

@@ -334,7 +334,7 @@
:app.rpc.doc/routes :app.rpc.doc/routes
{:app.rpc/methods (ig/ref :app.rpc/methods)} {:app.rpc/methods (ig/ref :app.rpc/methods)}
:app.rpc/routes ::rpc/routes
{::rpc/methods (ig/ref :app.rpc/methods) {::rpc/methods (ig/ref :app.rpc/methods)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::session/manager (ig/ref ::session/manager) ::session/manager (ig/ref ::session/manager)
@@ -426,6 +426,9 @@
;; module requires the migrations to run before initialize. ;; module requires the migrations to run before initialize.
::migrations (ig/ref :app.migrations/migrations)} ::migrations (ig/ref :app.migrations/migrations)}
::setup/clock
{}
:app.loggers.audit.archive-task/handler :app.loggers.audit.archive-task/handler
{::setup/props (ig/ref ::setup/props) {::setup/props (ig/ref ::setup/props)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)

View File

@@ -23,14 +23,14 @@
(dissoc row :perms)) (dissoc row :perms))
(defn create-access-token (defn create-access-token
[{:keys [::db/conn ::setup/props]} profile-id name expiration] [{:keys [::db/conn] :as cfg} profile-id name expiration]
(let [created-at (ct/now) (let [token-id (uuid/next)
token-id (uuid/next) expires-at (some-> expiration (ct/in-future))
token (tokens/generate props {:iss "access-token" created-at (ct/now)
:tid token-id token (tokens/generate cfg {:iss "access-token"
:iat created-at}) :iat created-at
:tid token-id})
expires-at (some-> expiration ct/in-future)
token (db/insert! conn :access-token token (db/insert! conn :access-token
{:id token-id {:id token-id
:name name :name name

View File

@@ -99,7 +99,7 @@
(profile/strip-private-attrs)) (profile/strip-private-attrs))
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) (tokens/verify cfg {:token token :iss :team-invitation}))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the ;; 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 ;; invitation because invitations matches exactly; and user can't login with other email and
@@ -153,7 +153,7 @@
(defn recover-profile (defn recover-profile
[{:keys [::db/conn] :as cfg} {:keys [token password]}] [{:keys [::db/conn] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token] (letfn [(validate-token [token]
(let [tdata (tokens/verify (::setup/props cfg) {:token token :iss :password-recovery})] (let [tdata (tokens/verify cfg {:token token :iss :password-recovery})]
(:profile-id tdata))) (:profile-id tdata)))
(update-password [conn profile-id] (update-password [conn profile-id]
@@ -192,7 +192,7 @@
:hint "registration disabled")) :hint "registration disabled"))
(when (contains? params :invitation-token) (when (contains? params :invitation-token)
(let [invitation (tokens/verify (::setup/props cfg) (let [invitation (tokens/verify cfg
{:token (:invitation-token params) {:token (:invitation-token params)
:iss :team-invitation})] :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation)) (when-not (= (:email params) (:member-email invitation))
@@ -249,7 +249,7 @@
:props {:newsletter-updates (or accept-newsletter-updates false)}} :props {:newsletter-updates (or accept-newsletter-updates false)}}
params (d/without-nils params) params (d/without-nils params)
token (tokens/generate (::setup/props cfg) params)] token (tokens/generate cfg params)]
(with-meta {:token token} (with-meta {:token token}
{::audit/profile-id uuid/zero}))) {::audit/profile-id uuid/zero})))
@@ -343,14 +343,14 @@
(defn send-email-verification! (defn send-email-verification!
[{:keys [::db/conn] :as cfg} profile] [{:keys [::db/conn] :as cfg} profile]
(let [vtoken (tokens/generate (::setup/props cfg) (let [vtoken (tokens/generate cfg
{:iss :verify-email {:iss :verify-email
:exp (ct/in-future "72h") :exp (ct/in-future "72h")
:profile-id (:id profile) :profile-id (:id profile)
:email (:email profile)}) :email (:email profile)})
;; NOTE: this token is mainly used for possible complains ;; NOTE: this token is mainly used for possible complains
;; identification on the sns webhook ;; identification on the sns webhook
ptoken (tokens/generate (::setup/props cfg) ptoken (tokens/generate cfg
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile) :profile-id (:id profile)
:exp (ct/in-future {:days 30})})] :exp (ct/in-future {:days 30})})]
@@ -364,7 +364,7 @@
(defn register-profile (defn register-profile
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token] :as params}] [{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register}) (let [claims (tokens/verify cfg {:token token :iss :prepared-register})
params (into claims params) params (into claims params)
profile (if-let [profile-id (:profile-id claims)] profile (if-let [profile-id (:profile-id claims)]
@@ -387,7 +387,7 @@
created? (-> profile meta :created true?) created? (-> profile meta :created true?)
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) (tokens/verify cfg {:token token :iss :team-invitation}))
props (-> (audit/profile->props profile) props (-> (audit/profile->props profile)
(assoc :from-invitation (some? invitation))) (assoc :from-invitation (some? invitation)))
@@ -420,7 +420,7 @@
(= (:email profile) (= (:email profile)
(:member-email invitation))) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile)) (let [claims (assoc invitation :member-id (:id profile))
token (tokens/generate (::setup/props cfg) claims)] token (tokens/generate cfg claims)]
(-> {:invitation-token token} (-> {:invitation-token token}
(rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/replace-props props (rph/with-meta {::audit/replace-props props
@@ -494,14 +494,14 @@
(defn- request-profile-recovery (defn- request-profile-recovery
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}] [{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}] (letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens/generate (::setup/props cfg) (let [token (tokens/generate cfg
{:iss :password-recovery {:iss :password-recovery
:exp (ct/in-future "15m") :exp (ct/in-future "15m")
:profile-id id})] :profile-id id})]
(assoc profile :token token))) (assoc profile :token token)))
(send-email-notification [conn profile] (send-email-notification [conn profile]
(let [ptoken (tokens/generate (::setup/props cfg) (let [ptoken (tokens/generate cfg
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile) :profile-id (:id profile)
:exp (ct/in-future {:days 30})})] :exp (ct/in-future {:days 30})})]

View File

@@ -38,7 +38,7 @@
::doc/added "1.15" ::doc/added "1.15"
::doc/module :auth ::doc/module :auth
::sm/params schema:login-with-ldap} ::sm/params schema:login-with-ldap}
[{:keys [::setup/props ::ldap/provider] :as cfg} params] [{:keys [::ldap/provider] :as cfg} params]
(when-not provider (when-not provider
(ex/raise :type :restriction (ex/raise :type :restriction
:code :ldap-not-initialized :code :ldap-not-initialized
@@ -60,11 +60,11 @@
;; user comes from team-invitation process; in this case, ;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation ;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged). ;; token (and mark current session as logged).
(let [claims (tokens/verify props {:token token :iss :team-invitation}) (let [claims (tokens/verify cfg {:token token :iss :team-invitation})
claims (assoc claims claims (assoc claims
:member-id (:id profile) :member-id (:id profile)
:member-email (:email profile)) :member-email (:email profile))
token (tokens/generate props claims)] token (tokens/generate cfg claims)]
(-> {:invitation-token token} (-> {:invitation-token token}
(rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-transform (session/create-fn cfg (:id profile)))
(rph/with-meta {::audit/props (:props profile) (rph/with-meta {::audit/props (:props profile)

View File

@@ -345,12 +345,12 @@
(defn- request-email-change! (defn- request-email-change!
[{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}]
(let [token (tokens/generate (::setup/props cfg) (let [token (tokens/generate cfg
{:iss :change-email {:iss :change-email
:exp (ct/in-future "15m") :exp (ct/in-future "15m")
:profile-id (:id profile) :profile-id (:id profile)
:email email}) :email email})
ptoken (tokens/generate (::setup/props cfg) ptoken (tokens/generate cfg
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile) :profile-id (:id profile)
:exp (ct/in-future {:days 30})})] :exp (ct/in-future {:days 30})})]

View File

@@ -43,7 +43,7 @@
(defn- create-invitation-token (defn- create-invitation-token
[cfg {:keys [profile-id valid-until team-id member-id member-email role]}] [cfg {:keys [profile-id valid-until team-id member-id member-email role]}]
(tokens/generate (::setup/props cfg) (tokens/generate cfg
{:iss :team-invitation {:iss :team-invitation
:exp valid-until :exp valid-until
:profile-id profile-id :profile-id profile-id
@@ -54,12 +54,8 @@
(defn- create-profile-identity-token (defn- create-profile-identity-token
[cfg profile-id] [cfg profile-id]
(assert (uuid? profile-id) "expected valid uuid for profile-id")
(dm/assert! (tokens/generate cfg
"expected valid uuid for profile-id"
(uuid? profile-id))
(tokens/generate (::setup/props cfg)
{:iss :profile-identity {:iss :profile-identity
:profile-id profile-id :profile-id profile-id
:exp (ct/in-future {:days 30})})) :exp (ct/in-future {:days 30})}))
@@ -522,7 +518,7 @@
(defn- check-existing-team-access-request (defn- check-existing-team-access-request
"Checks if an existing team access request is still valid" "Checks if an existing team access request is still valid"
[conn team-id profile-id] [{:keys [::db/conn]} team-id profile-id]
(when-let [request (db/get* conn :team-access-request (when-let [request (db/get* conn :team-access-request
{:team-id team-id {:team-id team-id
:requester-id profile-id})] :requester-id profile-id})]
@@ -540,8 +536,8 @@
(defn- upsert-team-access-request (defn- upsert-team-access-request
"Create or update team access request for provided team and profile-id" "Create or update team access request for provided team and profile-id"
[conn team-id requester-id] [{:keys [::db/conn] :as cfg} team-id requester-id]
(check-existing-team-access-request conn team-id requester-id) (check-existing-team-access-request cfg team-id requester-id)
(let [valid-until (ct/in-future {:hours 24}) (let [valid-until (ct/in-future {:hours 24})
auto-join-until (ct/in-future {:days 7}) auto-join-until (ct/in-future {:days 7})
request-id (uuid/next)] request-id (uuid/next)]
@@ -603,7 +599,7 @@
(teams/check-email-bounce conn (:email team-owner) false) (teams/check-email-bounce conn (:email team-owner) false)
(teams/check-email-spam conn (:email team-owner) true) (teams/check-email-spam conn (:email team-owner) true)
(let [request (upsert-team-access-request conn team-id profile-id) (let [request (upsert-team-access-request cfg team-id profile-id)
factory (cond factory (cond
(and (some? file) (:is-default team) is-viewer) (and (some? file) (:is-default team) is-viewer)
eml/request-file-access-yourpenpot-view eml/request-file-access-yourpenpot-view

View File

@@ -38,7 +38,7 @@
::doc/module :auth ::doc/module :auth
::sm/params schema:verify-token} ::sm/params schema:verify-token}
[cfg {:keys [token] :as params}] [cfg {:keys [token] :as params}]
(let [claims (tokens/verify (::setup/props cfg) {:token token})] (let [claims (tokens/verify cfg {:token token})]
(db/tx-run! cfg process-token params claims))) (db/tx-run! cfg process-token params claims)))
(defmethod process-token :change-email (defmethod process-token :change-email

View File

@@ -0,0 +1,48 @@
;; 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.setup.clock
"A service/module that manages the system clock and allows runtime
modification of time offset (useful for testing and time adjustments)."
(:require
[app.common.logging :as l]
[app.common.time :as ct]
[app.setup :as-alias setup]
[integrant.core :as ig])
(:import
java.time.Clock
java.time.Duration))
(defonce current
(atom {:clock (Clock/systemDefaultZone)
:offset nil}))
(defmethod ig/init-key ::setup/clock
[_ _]
(add-watch current ::common
(fn [_ _ _ {:keys [clock offset]}]
(let [clock (if (ct/duration? offset)
(Clock/offset ^Clock clock
^Duration offset)
clock)]
(l/wrn :hint "altering clock" :clock (str clock))
(alter-var-root #'ct/*clock* (constantly clock))))))
(defmethod ig/halt-key! ::setup/clock
[_ _]
(remove-watch current ::common))
(defn set-offset!
[duration]
(swap! current assoc :offset (some-> duration ct/duration)))
(defn set-clock!
([]
(swap! current assoc :clock (Clock/systemDefaultZone)))
([clock]
(when (instance? Clock clock)
(swap! current assoc :clock clock))))

View File

@@ -129,8 +129,7 @@
(defmethod exec-command "authenticate" (defmethod exec-command "authenticate"
[{:keys [token]}] [{:keys [token]}]
(when-let [system (get-current-system)] (when-let [system (get-current-system)]
(let [props (get system ::setup/props)] (tokens/verify system {:token token :iss "authentication"})))
(tokens/verify props {:token token :iss "authentication"}))))
(def ^:private schema:get-customer (def ^:private schema:get-customer
[:map [:id ::sm/uuid]]) [:map [:id ::sm/uuid]])

View File

@@ -8,33 +8,40 @@
"Tokens generation API." "Tokens generation API."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.transit :as t] [app.common.transit :as t]
[app.setup :as-alias setup]
[buddy.sign.jwe :as jwe])) [buddy.sign.jwe :as jwe]))
(defn generate (defn generate
[{:keys [tokens-key]} claims] [{:keys [::setup/props] :as cfg} claims]
(assert (contains? cfg ::setup/props))
(dm/assert! (let [tokens-key
"expexted token-key to be bytes instance" (get props :tokens-key)
(bytes? tokens-key))
(let [payload (-> claims payload
(assoc :iat (ct/now)) (-> claims
(update :iat (fn [v] (or v (ct/now))))
(d/without-nils) (d/without-nils)
(t/encode))] (t/encode))]
(jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm}))) (jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm})))
(defn decode (defn decode
[{:keys [tokens-key]} token] [{:keys [::setup/props] :as cfg} token]
(let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})] (let [tokens-key
(get props :tokens-key)
payload
(jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})]
(t/decode payload))) (t/decode payload)))
(defn verify (defn verify
[sprops {:keys [token] :as params}] [cfg {:keys [token] :as params}]
(let [claims (decode sprops token)] (let [claims (decode cfg token)]
(when (and (ct/inst? (:exp claims)) (when (and (ct/inst? (:exp claims))
(ct/is-before? (:exp claims) (ct/now))) (ct/is-before? (:exp claims) (ct/now)))
(ex/raise :type :validation (ex/raise :type :validation

View File

@@ -101,12 +101,10 @@
(t/deftest test-parse-bounce-report (t/deftest test-parse-bounce-report
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
props (:app.setup/props th/*system*) report (bounce-report {:token (tokens/generate th/*system*
cfg {:app.setup/props props}
report (bounce-report {:token (tokens/generate props
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})}) :profile-id (:id profile)})})
result (#'awsns/parse-notification cfg report)] result (#'awsns/parse-notification th/*system* report)]
;; (pprint result) ;; (pprint result)
(t/is (= "bounce" (:type result))) (t/is (= "bounce" (:type result)))
@@ -117,12 +115,10 @@
(t/deftest test-parse-complaint-report (t/deftest test-parse-complaint-report
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
props (:app.setup/props th/*system*) report (complaint-report {:token (tokens/generate th/*system*
cfg {:app.setup/props props}
report (complaint-report {:token (tokens/generate props
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})}) :profile-id (:id profile)})})
result (#'awsns/parse-notification cfg report)] result (#'awsns/parse-notification th/*system* report)]
;; (pprint result) ;; (pprint result)
(t/is (= "complaint" (:type result))) (t/is (= "complaint" (:type result)))
(t/is (= "abuse" (:kind result))) (t/is (= "abuse" (:kind result)))
@@ -143,15 +139,13 @@
(t/deftest test-process-bounce-report (t/deftest test-process-bounce-report
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
props (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*) pool (:app.db/pool th/*system*)
cfg {:app.setup/props props :app.db/pool pool} report (bounce-report {:token (tokens/generate th/*system*
report (bounce-report {:token (tokens/generate props
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})}) :profile-id (:id profile)})})
report (#'awsns/parse-notification cfg report)] report (#'awsns/parse-notification th/*system* report)]
(#'awsns/process-report cfg report) (#'awsns/process-report th/*system* report)
(let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)}) (let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)})
(mapv decode-row))] (mapv decode-row))]
@@ -170,16 +164,13 @@
(t/deftest test-process-complaint-report (t/deftest test-process-complaint-report
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
props (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*) pool (:app.db/pool th/*system*)
cfg {:app.setup/props props report (complaint-report {:token (tokens/generate th/*system*
:app.db/pool pool}
report (complaint-report {:token (tokens/generate props
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})}) :profile-id (:id profile)})})
report (#'awsns/parse-notification cfg report)] report (#'awsns/parse-notification th/*system* report)]
(#'awsns/process-report cfg report) (#'awsns/process-report th/*system* report)
(let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)}) (let [rows (->> (db/query pool :profile-complaint-report {:profile-id (:id profile)})
(mapv decode-row))] (mapv decode-row))]
@@ -200,16 +191,14 @@
(t/deftest test-process-bounce-report-to-self (t/deftest test-process-bounce-report-to-self
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
props (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*) pool (:app.db/pool th/*system*)
cfg {:app.setup/props props :app.db/pool pool}
report (bounce-report {:email (:email profile) report (bounce-report {:email (:email profile)
:token (tokens/generate props :token (tokens/generate th/*system*
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})}) :profile-id (:id profile)})})
report (#'awsns/parse-notification cfg report)] report (#'awsns/parse-notification th/*system* report)]
(#'awsns/process-report cfg report) (#'awsns/process-report th/*system* report)
(let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})] (let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})]
(t/is (= 1 (count rows)))) (t/is (= 1 (count rows))))
@@ -222,16 +211,14 @@
(t/deftest test-process-complaint-report-to-self (t/deftest test-process-complaint-report-to-self
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
props (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*) pool (:app.db/pool th/*system*)
cfg {:app.setup/props props :app.db/pool pool}
report (complaint-report {:email (:email profile) report (complaint-report {:email (:email profile)
:token (tokens/generate props :token (tokens/generate th/*system*
{:iss :profile-identity {:iss :profile-identity
:profile-id (:id profile)})}) :profile-id (:id profile)})})
report (#'awsns/parse-notification cfg report)] report (#'awsns/parse-notification th/*system* report)]
(#'awsns/process-report cfg report) (#'awsns/process-report th/*system* report)
(let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})] (let [rows (db/query pool :profile-complaint-report {:profile-id (:id profile)})]
(t/is (= 1 (count rows)))) (t/is (= 1 (count rows))))

View File

@@ -25,8 +25,7 @@
(t/deftest authenticate-method (t/deftest authenticate-method
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
props (get th/*system* :app.setup/props) token (#'sess/gen-token th/*system* {:profile-id (:id profile)})
token (#'sess/gen-token props {:profile-id (:id profile)})
request {:params {:token token}} request {:params {:token token}}
response (#'mgmt/authenticate th/*system* request)] response (#'mgmt/authenticate th/*system* request)]

View File

@@ -514,8 +514,7 @@
(t/is (= 0 (:call-count @mock)))))))) (t/is (= 0 (:call-count @mock))))))))
(t/deftest prepare-and-register-with-invitation-and-enabled-registration-1 (t/deftest prepare-and-register-with-invitation-and-enabled-registration-1
(let [sprops (:app.setup/props th/*system*) (let [itoken (tokens/generate th/*system*
itoken (tokens/generate sprops
{:iss :team-invitation {:iss :team-invitation
:exp (ct/in-future "48h") :exp (ct/in-future "48h")
:role :editor :role :editor
@@ -543,8 +542,7 @@
(t/is (string? (:invitation-token result)))))) (t/is (string? (:invitation-token result))))))
(t/deftest prepare-and-register-with-invitation-and-enabled-registration-2 (t/deftest prepare-and-register-with-invitation-and-enabled-registration-2
(let [sprops (:app.setup/props th/*system*) (let [itoken (tokens/generate th/*system*
itoken (tokens/generate sprops
{:iss :team-invitation {:iss :team-invitation
:exp (ct/in-future "48h") :exp (ct/in-future "48h")
:role :editor :role :editor
@@ -565,8 +563,7 @@
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-1 (t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
(with-redefs [app.config/flags [:disable-registration]] (with-redefs [app.config/flags [:disable-registration]]
(let [sprops (:app.setup/props th/*system*) (let [itoken (tokens/generate th/*system*
itoken (tokens/generate sprops
{:iss :team-invitation {:iss :team-invitation
:exp (ct/in-future "48h") :exp (ct/in-future "48h")
:role :editor :role :editor
@@ -586,8 +583,7 @@
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-2 (t/deftest prepare-and-register-with-invitation-and-disabled-registration-2
(with-redefs [app.config/flags [:disable-registration]] (with-redefs [app.config/flags [:disable-registration]]
(let [sprops (:app.setup/props th/*system*) (let [itoken (tokens/generate th/*system*
itoken (tokens/generate sprops
{:iss :team-invitation {:iss :team-invitation
:exp (ct/in-future "48h") :exp (ct/in-future "48h")
:role :editor :role :editor
@@ -608,8 +604,7 @@
(t/deftest prepare-and-register-with-invitation-and-disabled-login-with-password (t/deftest prepare-and-register-with-invitation-and-disabled-login-with-password
(with-redefs [app.config/flags [:disable-login-with-password]] (with-redefs [app.config/flags [:disable-login-with-password]]
(let [sprops (:app.setup/props th/*system*) (let [itoken (tokens/generate th/*system*
itoken (tokens/generate sprops
{:iss :team-invitation {:iss :team-invitation
:exp (ct/in-future "48h") :exp (ct/in-future "48h")
:role :editor :role :editor

View File

@@ -208,8 +208,6 @@
profile2 (th/create-profile* 2 {:is-active true}) profile2 (th/create-profile* 2 {:is-active true})
team (th/create-team* 1 {:profile-id (:id profile1)}) team (th/create-team* 1 {:profile-id (:id profile1)})
sprops (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*)] pool (:app.db/pool th/*system*)]
;; Try to invite a not existing user ;; Try to invite a not existing user
@@ -226,7 +224,7 @@
(t/is (= 1 (-> out :result :total))) (t/is (= 1 (-> out :result :total)))
(let [token (-> out :result :invitations first) (let [token (-> out :result :invitations first)
claims (tokens/decode sprops token)] claims (tokens/decode th/*system* token)]
(t/is (= :team-invitation (:iss claims))) (t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims))) (t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims))) (t/is (= :editor (:role claims)))
@@ -250,7 +248,7 @@
(t/is (= 1 (-> out :result :total))) (t/is (= 1 (-> out :result :total)))
(let [token (-> out :result :invitations first) (let [token (-> out :result :invitations first)
claims (tokens/decode sprops token)] claims (tokens/decode th/*system* token)]
(t/is (= :team-invitation (:iss claims))) (t/is (= :team-invitation (:iss claims)))
(t/is (= (:id profile1) (:profile-id claims))) (t/is (= (:id profile1) (:profile-id claims)))
(t/is (= :editor (:role claims))) (t/is (= :editor (:role claims)))
@@ -266,10 +264,9 @@
team (th/create-team* 1 {:profile-id (:id profile1)}) team (th/create-team* 1 {:profile-id (:id profile1)})
sprops (:app.setup/props th/*system*)
pool (:app.db/pool th/*system*)] pool (:app.db/pool th/*system*)]
(let [token (tokens/generate sprops (let [token (tokens/generate th/*system*
{:iss :team-invitation {:iss :team-invitation
:exp (ct/in-future "1h") :exp (ct/in-future "1h")
:profile-id (:id profile1) :profile-id (:id profile1)

View File

@@ -52,6 +52,7 @@
[cuerdas.core :as str]) [cuerdas.core :as str])
#?(:clj #?(:clj
(:import (:import
java.time.Clock
java.time.Duration java.time.Duration
java.time.Instant java.time.Instant
java.time.OffsetDateTime java.time.OffsetDateTime
@@ -63,9 +64,11 @@
java.time.temporal.TemporalAmount java.time.temporal.TemporalAmount
java.time.temporal.TemporalUnit))) java.time.temporal.TemporalUnit)))
#?(:clj (def ^:dynamic *clock* (Clock/systemDefaultZone)))
(defn now (defn now
[] []
#?(:clj (Instant/now) #?(:clj (Instant/now *clock*)
:cljs (new js/Date))) :cljs (new js/Date)))
;; --- DURATION ;; --- DURATION