From bd63598185daec194ae3161711dbc47e8730ad84 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 3 Oct 2025 11:44:51 +0200 Subject: [PATCH 1/2] :tada: Add virtual clock implementation --- backend/src/app/http/session.clj | 15 +++++----- backend/src/app/main.clj | 5 +++- backend/src/app/setup/clock.clj | 48 ++++++++++++++++++++++++++++++++ common/src/app/common/time.cljc | 24 +++++++++++----- 4 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 backend/src/app/setup/clock.clj diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index d3bc0bd2bd..9bcb459b8d 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -332,16 +332,16 @@ (def ^:private sql:delete-expired - "delete from http_session - where updated_at < now() - ?::interval + "DELETE FROM http_session + WHERE updated_at < ?::timestamptz or (updated_at is null and - created_at < now() - ?::interval)") + created_at < ?::timestamptz)") (defn- collect-expired-tasks [{:keys [::db/conn ::tasks/max-age]}] - (let [interval (db/interval max-age) - result (db/exec-one! conn [sql:delete-expired interval interval]) - result (:next.jdbc/update-count result)] + (let [threshold (ct/minus (ct/now) max-age) + result (-> (db/exec-one! conn [sql:delete-expired threshold threshold]) + (db/get-update-count))] (l/debug :task "gc" :hint "clean http sessions" :deleted result) @@ -350,4 +350,5 @@ (defmethod ig/init-key ::tasks/gc [_ {:keys [::tasks/max-age] :as cfg}] (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))) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 64150b42b1..d6f4cfc4f5 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -334,7 +334,7 @@ :app.rpc.doc/routes {:app.rpc/methods (ig/ref :app.rpc/methods)} - :app.rpc/routes + ::rpc/routes {::rpc/methods (ig/ref :app.rpc/methods) ::db/pool (ig/ref ::db/pool) ::session/manager (ig/ref ::session/manager) @@ -426,6 +426,9 @@ ;; module requires the migrations to run before initialize. ::migrations (ig/ref :app.migrations/migrations)} + ::setup/clock + {} + :app.loggers.audit.archive-task/handler {::setup/props (ig/ref ::setup/props) ::db/pool (ig/ref ::db/pool) diff --git a/backend/src/app/setup/clock.clj b/backend/src/app/setup/clock.clj new file mode 100644 index 0000000000..09ab991856 --- /dev/null +++ b/backend/src/app/setup/clock.clj @@ -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)))) diff --git a/common/src/app/common/time.cljc b/common/src/app/common/time.cljc index 3fe0350bd0..c4489f1a88 100644 --- a/common/src/app/common/time.cljc +++ b/common/src/app/common/time.cljc @@ -52,6 +52,7 @@ [cuerdas.core :as str]) #?(:clj (:import + java.time.Clock java.time.Duration java.time.Instant java.time.OffsetDateTime @@ -63,10 +64,15 @@ java.time.temporal.TemporalAmount java.time.temporal.TemporalUnit))) +#?(:clj (def ^:dynamic *clock* (Clock/systemDefaultZone))) + (defn now - [] - #?(:clj (Instant/now) - :cljs (new js/Date))) + ([] + #?(:clj (Instant/now *clock*) + :cljs (new js/Date))) + ([clock] + #?(:cljs (throw (js/Error. "not implemented" {:clock clock})) + :clj (Instant/now ^Clock clock)))) ;; --- DURATION @@ -319,12 +325,16 @@ :cljs (js/Error. "unsupported type")))))) (defn in-future - [v] - (plus (now) v)) + ([v] + (plus (now) v)) + ([clock v] + (plus (now clock) v))) (defn in-past - [v] - (minus (now) v)) + ([v] + (minus (now) v)) + ([clock v] + (minus (now clock) v))) #?(:clj (defn diff From 61d9b57bc76ae24651d3002073c772d69be4d784 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 3 Oct 2025 11:46:37 +0200 Subject: [PATCH 2/2] :recycle: Refactor internal tokens API Mainly make it receive the whol cfg/system instead only props. This makes the api more flexible for a future extending without the need to change the api again. --- backend/resources/app/templates/debug.tmpl | 34 ++++++++++++++ backend/src/app/auth/oidc.clj | 11 +++-- backend/src/app/http/access_token.clj | 8 ++-- backend/src/app/http/awsns.clj | 2 +- backend/src/app/http/debug.clj | 34 +++++++++++--- backend/src/app/http/management.clj | 3 +- backend/src/app/http/session.clj | 40 ++++++++--------- .../src/app/loggers/audit/archive_task.clj | 4 +- backend/src/app/rpc/commands/access_token.clj | 14 +++--- backend/src/app/rpc/commands/auth.clj | 22 ++++----- backend/src/app/rpc/commands/ldap.clj | 6 +-- backend/src/app/rpc/commands/profile.clj | 4 +- .../app/rpc/commands/teams_invitations.clj | 18 +++----- backend/src/app/rpc/commands/verify_token.clj | 2 +- backend/src/app/srepl/cli.clj | 3 +- backend/src/app/tokens.clj | 33 ++++++++------ .../backend_tests/bounce_handling_test.clj | 45 +++++++------------ .../backend_tests/http_management_test.clj | 3 +- .../test/backend_tests/rpc_profile_test.clj | 15 +++---- backend/test/backend_tests/rpc_team_test.clj | 9 ++-- common/src/app/common/time.cljc | 21 +++------ 21 files changed, 178 insertions(+), 153 deletions(-) diff --git a/backend/resources/app/templates/debug.tmpl b/backend/resources/app/templates/debug.tmpl index 65d3d7614c..1f803140a2 100644 --- a/backend/resources/app/templates/debug.tmpl +++ b/backend/resources/app/templates/debug.tmpl @@ -45,7 +45,41 @@ Debug Main Page +
+ VIRTUAL CLOCK + +

+ CURRENT CLOCK: {{current-clock}} +
+ CURRENT OFFSET: {{current-offset}} +
+ CURRENT TIME: {{current-time}} +

+ +

Examples: 3h, -7h, 24h (allowed suffixes: h, s)

+
+ +
+
+ +
+ +
+ + +
+ + This is a just a security double check for prevent non intentional submits. + +
+ +
+ + +
+
+
diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 1651517083..9f5de73957 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -434,10 +434,10 @@ (sm/validator schema: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) 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) info (case (cf/get :oidc-user-info-source) :token (get-user-info cfg tdata) @@ -516,7 +516,7 @@ :iss :prepared-register :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)) :fullname (:fullname info)} params (d/without-nils params)] @@ -569,7 +569,7 @@ :else (let [sxf (session/create-fn cfg (:id profile)) token (or (:invitation-token info) - (tokens/generate (::setup/props cfg) + (tokens/generate cfg {:iss :auth :exp (ct/in-future "15m") :profile-id (:id profile)})) @@ -620,8 +620,7 @@ :external-session-id esid :props props :exp (ct/in-future "4h")} - state (tokens/generate (::setup/props cfg) - (d/without-nils params)) + state (tokens/generate cfg (d/without-nils params)) uri (build-auth-uri cfg state)] {::yres/status 200 ::yres/body {:redirect-uri uri}})) diff --git a/backend/src/app/http/access_token.clj b/backend/src/app/http/access_token.clj index d498005642..b9b85a8b57 100644 --- a/backend/src/app/http/access_token.clj +++ b/backend/src/app/http/access_token.clj @@ -23,9 +23,9 @@ (second))) (defn- decode-token - [props token] + [cfg token] (when token - (tokens/verify props {:token token :iss "access-token"}))) + (tokens/verify cfg {:token token :iss "access-token"}))) (def sql:get-token-data "SELECT perms, profile_id, expires_at @@ -43,11 +43,11 @@ (defn- wrap-soft-auth "Soft Authentication, will be executed synchronously on the undertow worker thread." - [handler {:keys [::setup/props]}] + [handler cfg] (letfn [(handle-request [request] (try (let [token (get-token request) - claims (decode-token props token)] + claims (decode-token cfg token)] (cond-> request (map? claims) (assoc ::id (:tid claims)))) diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index 0d3f1d4066..051a2ecf8b 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -107,7 +107,7 @@ [cfg headers] (let [tdata (get headers "x-penpot-data")] (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))))) (defn- parse-notification diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 76d7661bdf..db57881812 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -27,6 +27,7 @@ [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.setup :as-alias setup] + [app.setup.clock :as clock] [app.srepl.main :as srepl] [app.storage :as-alias sto] [app.storage.tmp :as tmp] @@ -49,11 +50,17 @@ (defn index-handler [_cfg _request] - {::yres/status 200 - ::yres/headers {"content-type" "text/html"} - ::yres/body (-> (io/resource "app/templates/debug.tmpl") - (tmpl/render {:version (:full cf/version) - :supported-features cfeat/supported-features}))}) + (let [{:keys [clock offset]} @clock/current] + {::yres/status 200 + ::yres/headers {"content-type" "text/html"} + ::yres/body (-> (io/resource "app/templates/debug.tmpl") + (tmpl/render {:version (:full cf/version) + :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 @@ -417,7 +424,6 @@ ::yres/headers {"content-type" "text/plain"} ::yres/body "OK"})) - (defn- handle-team-features [cfg {:keys [params] :as request}] (let [team-id (some-> params :team-id d/parse-uuid) @@ -462,6 +468,20 @@ ::yres/headers {"content-type" "text/plain"} ::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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -548,6 +568,8 @@ ["/error/:id" {:handler (partial error-handler cfg)}] ["/error" {:handler (partial error-list-handler cfg)}] ["/actions" {:middleware [[errors]]} + ["/set-virtual-clock" + {:handler (partial set-virtual-clock cfg)}] ["/resend-email-verification" {:handler (partial resend-email-notification cfg)}] ["/reset-file-version" diff --git a/backend/src/app/http/management.clj b/backend/src/app/http/management.clj index 929cda32fd..f88dadc16c 100644 --- a/backend/src/app/http/management.clj +++ b/backend/src/app/http/management.clj @@ -79,8 +79,7 @@ (defn- authenticate [cfg request] (let [token (-> request :params :token) - props (get cfg ::setup/props) - result (tokens/verify props {:token token :iss "authentication"})] + result (tokens/verify cfg {:token token :iss "authentication"})] {::yres/status 200 ::yres/body result})) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 9bcb459b8d..4b4ad29a99 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -72,7 +72,7 @@ (sm/validator schema:params)) (defn- prepare-session-params - [key params] + [params key] (assert (string? key) "expected key to be a string") (assert (not (str/blank? key)) "expected key to be not empty") (assert (valid-params? params) "expected valid params") @@ -90,7 +90,9 @@ (db/exec-one! pool (sql/select :http-session {:id token}))) (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) params)) @@ -113,7 +115,9 @@ (get @cache token)) (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) params)) @@ -150,16 +154,15 @@ (declare ^:private gen-token) (defn create-fn - [{:keys [::manager ::setup/props]} profile-id] + [{:keys [::manager] :as cfg} profile-id] (assert (manager? manager) "expected valid session manager") (assert (uuid? profile-id) "expected valid uuid for profile-id") (fn [request response] (let [uagent (yreq/get-header request "user-agent") params {:profile-id profile-id - :user-agent uagent - :created-at (ct/now)} - token (gen-token props params) + :user-agent uagent} + token (gen-token cfg params) session (write! manager token params)] (l/trace :hint "create" :profile-id (str profile-id)) (-> response @@ -181,14 +184,14 @@ (clear-auth-data-cookie))))) (defn- gen-token - [props {:keys [profile-id created-at]}] - (tokens/generate props {:iss "authentication" - :iat created-at - :uid profile-id})) + [cfg {:keys [profile-id created-at]}] + (tokens/generate cfg {:iss "authentication" + :iat created-at + :uid profile-id})) (defn- decode-token - [props token] + [cfg token] (when token - (tokens/verify props {:token token :iss "authentication"}))) + (tokens/verify cfg {:token token :iss "authentication"}))) (defn- get-token [request] @@ -208,12 +211,12 @@ (neg? (compare default-renewal-max-age elapsed))))) (defn- wrap-soft-auth - [handler {:keys [::manager ::setup/props]}] + [handler {:keys [::manager] :as cfg}] (assert (manager? manager) "expected valid session manager") (letfn [(handle-request [request] (try (let [token (get-token request) - claims (decode-token props token)] + claims (decode-token cfg token)] (cond-> request (map? claims) (-> (assoc ::token-claims claims) @@ -256,7 +259,7 @@ (defn- assign-auth-token-cookie [response {token :id updated-at :updated-at}] (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) expires (ct/plus created-at max-age) secure? (contains? cf/flags :secure-session-cookies) @@ -279,7 +282,7 @@ domain (cf/get :auth-data-cookie-domain) 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) expires (ct/plus created-at max-age) @@ -313,13 +316,10 @@ (string? domain) (update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0})))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TASK: SESSION GC ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; FIXME: MOVE - (defmethod ig/assert-key ::tasks/gc [_ params] (assert (db/pool? (::db/pool params)) "expected valid database pool") diff --git a/backend/src/app/loggers/audit/archive_task.clj b/backend/src/app/loggers/audit/archive_task.clj index 257f55a94e..c8aa0e0de9 100644 --- a/backend/src/app/loggers/audit/archive_task.clj +++ b/backend/src/app/loggers/audit/archive_task.clj @@ -9,7 +9,6 @@ [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.schema :as sm] - [app.common.time :as ct] [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] @@ -53,9 +52,8 @@ (defn- send! [{:keys [::uri] :as cfg} events] - (let [token (tokens/generate (::setup/props cfg) + (let [token (tokens/generate cfg {:iss "authentication" - :iat (ct/now) :uid uuid/zero}) body (t/encode {:events events}) headers {"content-type" "application/transit+json" diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index d91e39c86b..fcbb0af2f2 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -23,14 +23,14 @@ (dissoc row :perms)) (defn create-access-token - [{:keys [::db/conn ::setup/props]} profile-id name expiration] - (let [created-at (ct/now) - token-id (uuid/next) - token (tokens/generate props {:iss "access-token" - :tid token-id - :iat created-at}) + [{:keys [::db/conn] :as cfg} profile-id name expiration] + (let [token-id (uuid/next) + expires-at (some-> expiration (ct/in-future)) + created-at (ct/now) + token (tokens/generate cfg {:iss "access-token" + :iat created-at + :tid token-id}) - expires-at (some-> expiration ct/in-future) token (db/insert! conn :access-token {:id token-id :name name diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index eb8621fd1b..f6b81d6add 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -99,7 +99,7 @@ (profile/strip-private-attrs)) 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 ;; invitation because invitations matches exactly; and user can't login with other email and @@ -153,7 +153,7 @@ (defn recover-profile [{:keys [::db/conn] :as cfg} {:keys [token password]}] (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))) (update-password [conn profile-id] @@ -192,7 +192,7 @@ :hint "registration disabled")) (when (contains? params :invitation-token) - (let [invitation (tokens/verify (::setup/props cfg) + (let [invitation (tokens/verify cfg {:token (:invitation-token params) :iss :team-invitation})] (when-not (= (:email params) (:member-email invitation)) @@ -249,7 +249,7 @@ :props {:newsletter-updates (or accept-newsletter-updates false)}} params (d/without-nils params) - token (tokens/generate (::setup/props cfg) params)] + token (tokens/generate cfg params)] (with-meta {:token token} {::audit/profile-id uuid/zero}))) @@ -343,14 +343,14 @@ (defn send-email-verification! [{:keys [::db/conn] :as cfg} profile] - (let [vtoken (tokens/generate (::setup/props cfg) + (let [vtoken (tokens/generate cfg {:iss :verify-email :exp (ct/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 (::setup/props cfg) + ptoken (tokens/generate cfg {:iss :profile-identity :profile-id (:id profile) :exp (ct/in-future {:days 30})})] @@ -364,7 +364,7 @@ (defn register-profile [{: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) profile (if-let [profile-id (:profile-id claims)] @@ -387,7 +387,7 @@ created? (-> profile meta :created true?) 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) (assoc :from-invitation (some? invitation))) @@ -420,7 +420,7 @@ (= (:email profile) (:member-email invitation))) (let [claims (assoc invitation :member-id (:id profile)) - token (tokens/generate (::setup/props cfg) claims)] + token (tokens/generate cfg claims)] (-> {:invitation-token token} (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/replace-props props @@ -494,14 +494,14 @@ (defn- request-profile-recovery [{:keys [::db/conn] :as cfg} {:keys [email] :as params}] (letfn [(create-recovery-token [{:keys [id] :as profile}] - (let [token (tokens/generate (::setup/props cfg) + (let [token (tokens/generate cfg {:iss :password-recovery :exp (ct/in-future "15m") :profile-id id})] (assoc profile :token token))) (send-email-notification [conn profile] - (let [ptoken (tokens/generate (::setup/props cfg) + (let [ptoken (tokens/generate cfg {:iss :profile-identity :profile-id (:id profile) :exp (ct/in-future {:days 30})})] diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index 0829987d4d..26524f208f 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -38,7 +38,7 @@ ::doc/added "1.15" ::doc/module :auth ::sm/params schema:login-with-ldap} - [{:keys [::setup/props ::ldap/provider] :as cfg} params] + [{:keys [::ldap/provider] :as cfg} params] (when-not provider (ex/raise :type :restriction :code :ldap-not-initialized @@ -60,11 +60,11 @@ ;; user comes from team-invitation process; in this case, ;; regenerate token and send back to the user a new invitation ;; 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 :member-id (:id profile) :member-email (:email profile)) - token (tokens/generate props claims)] + token (tokens/generate cfg claims)] (-> {:invitation-token token} (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (:props profile) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 747c1c4f50..b078c2adad 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -345,12 +345,12 @@ (defn- request-email-change! [{: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 :exp (ct/in-future "15m") :profile-id (:id profile) :email email}) - ptoken (tokens/generate (::setup/props cfg) + ptoken (tokens/generate cfg {:iss :profile-identity :profile-id (:id profile) :exp (ct/in-future {:days 30})})] diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 8dc7f70d44..f7a077068d 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -43,7 +43,7 @@ (defn- create-invitation-token [cfg {:keys [profile-id valid-until team-id member-id member-email role]}] - (tokens/generate (::setup/props cfg) + (tokens/generate cfg {:iss :team-invitation :exp valid-until :profile-id profile-id @@ -54,12 +54,8 @@ (defn- create-profile-identity-token [cfg profile-id] - - (dm/assert! - "expected valid uuid for profile-id" - (uuid? profile-id)) - - (tokens/generate (::setup/props cfg) + (assert (uuid? profile-id) "expected valid uuid for profile-id") + (tokens/generate cfg {:iss :profile-identity :profile-id profile-id :exp (ct/in-future {:days 30})})) @@ -522,7 +518,7 @@ (defn- check-existing-team-access-request "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 {:team-id team-id :requester-id profile-id})] @@ -540,8 +536,8 @@ (defn- upsert-team-access-request "Create or update team access request for provided team and profile-id" - [conn team-id requester-id] - (check-existing-team-access-request conn team-id requester-id) + [{:keys [::db/conn] :as cfg} team-id requester-id] + (check-existing-team-access-request cfg team-id requester-id) (let [valid-until (ct/in-future {:hours 24}) auto-join-until (ct/in-future {:days 7}) request-id (uuid/next)] @@ -603,7 +599,7 @@ (teams/check-email-bounce conn (:email team-owner) false) (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 (and (some? file) (:is-default team) is-viewer) eml/request-file-access-yourpenpot-view diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index ff59a5eead..b650e6a8b9 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -38,7 +38,7 @@ ::doc/module :auth ::sm/params schema:verify-token} [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))) (defmethod process-token :change-email diff --git a/backend/src/app/srepl/cli.clj b/backend/src/app/srepl/cli.clj index 48876c905e..4df356752d 100644 --- a/backend/src/app/srepl/cli.clj +++ b/backend/src/app/srepl/cli.clj @@ -129,8 +129,7 @@ (defmethod exec-command "authenticate" [{:keys [token]}] (when-let [system (get-current-system)] - (let [props (get system ::setup/props)] - (tokens/verify props {:token token :iss "authentication"})))) + (tokens/verify system {:token token :iss "authentication"}))) (def ^:private schema:get-customer [:map [:id ::sm/uuid]]) diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj index 14415d0892..d08b8be58f 100644 --- a/backend/src/app/tokens.clj +++ b/backend/src/app/tokens.clj @@ -8,33 +8,40 @@ "Tokens generation API." (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.time :as ct] [app.common.transit :as t] + [app.setup :as-alias setup] [buddy.sign.jwe :as jwe])) (defn generate - [{:keys [tokens-key]} claims] + [{:keys [::setup/props] :as cfg} claims] + (assert (contains? cfg ::setup/props)) - (dm/assert! - "expexted token-key to be bytes instance" - (bytes? tokens-key)) + (let [tokens-key + (get props :tokens-key) + + payload + (-> claims + (update :iat (fn [v] (or v (ct/now)))) + (d/without-nils) + (t/encode))] - (let [payload (-> claims - (assoc :iat (ct/now)) - (d/without-nils) - (t/encode))] (jwe/encrypt payload tokens-key {:alg :a256kw :enc :a256gcm}))) (defn decode - [{:keys [tokens-key]} token] - (let [payload (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})] + [{:keys [::setup/props] :as cfg} token] + (let [tokens-key + (get props :tokens-key) + + payload + (jwe/decrypt token tokens-key {:alg :a256kw :enc :a256gcm})] + (t/decode payload))) (defn verify - [sprops {:keys [token] :as params}] - (let [claims (decode sprops token)] + [cfg {:keys [token] :as params}] + (let [claims (decode cfg token)] (when (and (ct/inst? (:exp claims)) (ct/is-before? (:exp claims) (ct/now))) (ex/raise :type :validation diff --git a/backend/test/backend_tests/bounce_handling_test.clj b/backend/test/backend_tests/bounce_handling_test.clj index 3df4789ad7..bef06ad6ff 100644 --- a/backend/test/backend_tests/bounce_handling_test.clj +++ b/backend/test/backend_tests/bounce_handling_test.clj @@ -101,12 +101,10 @@ (t/deftest test-parse-bounce-report (let [profile (th/create-profile* 1) - props (:app.setup/props th/*system*) - cfg {:app.setup/props props} - report (bounce-report {:token (tokens/generate props + report (bounce-report {:token (tokens/generate th/*system* {:iss :profile-identity :profile-id (:id profile)})}) - result (#'awsns/parse-notification cfg report)] + result (#'awsns/parse-notification th/*system* report)] ;; (pprint result) (t/is (= "bounce" (:type result))) @@ -117,12 +115,10 @@ (t/deftest test-parse-complaint-report (let [profile (th/create-profile* 1) - props (:app.setup/props th/*system*) - cfg {:app.setup/props props} - report (complaint-report {:token (tokens/generate props + report (complaint-report {:token (tokens/generate th/*system* {:iss :profile-identity :profile-id (:id profile)})}) - result (#'awsns/parse-notification cfg report)] + result (#'awsns/parse-notification th/*system* report)] ;; (pprint result) (t/is (= "complaint" (:type result))) (t/is (= "abuse" (:kind result))) @@ -143,15 +139,13 @@ (t/deftest test-process-bounce-report (let [profile (th/create-profile* 1) - props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.setup/props props :app.db/pool pool} - report (bounce-report {:token (tokens/generate props + report (bounce-report {:token (tokens/generate th/*system* {:iss :profile-identity :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)}) (mapv decode-row))] @@ -170,16 +164,13 @@ (t/deftest test-process-complaint-report (let [profile (th/create-profile* 1) - props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.setup/props props - :app.db/pool pool} - report (complaint-report {:token (tokens/generate props + report (complaint-report {:token (tokens/generate th/*system* {:iss :profile-identity :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)}) (mapv decode-row))] @@ -200,16 +191,14 @@ (t/deftest test-process-bounce-report-to-self (let [profile (th/create-profile* 1) - props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.setup/props props :app.db/pool pool} report (bounce-report {:email (:email profile) - :token (tokens/generate props + :token (tokens/generate th/*system* {:iss :profile-identity :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)})] (t/is (= 1 (count rows)))) @@ -222,16 +211,14 @@ (t/deftest test-process-complaint-report-to-self (let [profile (th/create-profile* 1) - props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.setup/props props :app.db/pool pool} report (complaint-report {:email (:email profile) - :token (tokens/generate props + :token (tokens/generate th/*system* {:iss :profile-identity :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)})] (t/is (= 1 (count rows)))) diff --git a/backend/test/backend_tests/http_management_test.clj b/backend/test/backend_tests/http_management_test.clj index a02bbe20d6..9e8f554017 100644 --- a/backend/test/backend_tests/http_management_test.clj +++ b/backend/test/backend_tests/http_management_test.clj @@ -25,8 +25,7 @@ (t/deftest authenticate-method (let [profile (th/create-profile* 1) - props (get th/*system* :app.setup/props) - token (#'sess/gen-token props {:profile-id (:id profile)}) + token (#'sess/gen-token th/*system* {:profile-id (:id profile)}) request {:params {:token token}} response (#'mgmt/authenticate th/*system* request)] diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 226df0dc61..654af973cd 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -514,8 +514,7 @@ (t/is (= 0 (:call-count @mock)))))))) (t/deftest prepare-and-register-with-invitation-and-enabled-registration-1 - (let [sprops (:app.setup/props th/*system*) - itoken (tokens/generate sprops + (let [itoken (tokens/generate th/*system* {:iss :team-invitation :exp (ct/in-future "48h") :role :editor @@ -543,8 +542,7 @@ (t/is (string? (:invitation-token result)))))) (t/deftest prepare-and-register-with-invitation-and-enabled-registration-2 - (let [sprops (:app.setup/props th/*system*) - itoken (tokens/generate sprops + (let [itoken (tokens/generate th/*system* {:iss :team-invitation :exp (ct/in-future "48h") :role :editor @@ -565,8 +563,7 @@ (t/deftest prepare-and-register-with-invitation-and-disabled-registration-1 (with-redefs [app.config/flags [:disable-registration]] - (let [sprops (:app.setup/props th/*system*) - itoken (tokens/generate sprops + (let [itoken (tokens/generate th/*system* {:iss :team-invitation :exp (ct/in-future "48h") :role :editor @@ -586,8 +583,7 @@ (t/deftest prepare-and-register-with-invitation-and-disabled-registration-2 (with-redefs [app.config/flags [:disable-registration]] - (let [sprops (:app.setup/props th/*system*) - itoken (tokens/generate sprops + (let [itoken (tokens/generate th/*system* {:iss :team-invitation :exp (ct/in-future "48h") :role :editor @@ -608,8 +604,7 @@ (t/deftest prepare-and-register-with-invitation-and-disabled-login-with-password (with-redefs [app.config/flags [:disable-login-with-password]] - (let [sprops (:app.setup/props th/*system*) - itoken (tokens/generate sprops + (let [itoken (tokens/generate th/*system* {:iss :team-invitation :exp (ct/in-future "48h") :role :editor diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 516abc824b..206925f341 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -208,8 +208,6 @@ profile2 (th/create-profile* 2 {:is-active true}) team (th/create-team* 1 {:profile-id (:id profile1)}) - - sprops (:app.setup/props th/*system*) pool (:app.db/pool th/*system*)] ;; Try to invite a not existing user @@ -226,7 +224,7 @@ (t/is (= 1 (-> out :result :total))) (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 (= (:id profile1) (:profile-id claims))) (t/is (= :editor (:role claims))) @@ -250,7 +248,7 @@ (t/is (= 1 (-> out :result :total))) (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 (= (:id profile1) (:profile-id claims))) (t/is (= :editor (:role claims))) @@ -266,10 +264,9 @@ team (th/create-team* 1 {:profile-id (:id profile1)}) - sprops (:app.setup/props th/*system*) pool (:app.db/pool th/*system*)] - (let [token (tokens/generate sprops + (let [token (tokens/generate th/*system* {:iss :team-invitation :exp (ct/in-future "1h") :profile-id (:id profile1) diff --git a/common/src/app/common/time.cljc b/common/src/app/common/time.cljc index c4489f1a88..53a44a23d9 100644 --- a/common/src/app/common/time.cljc +++ b/common/src/app/common/time.cljc @@ -67,12 +67,9 @@ #?(:clj (def ^:dynamic *clock* (Clock/systemDefaultZone))) (defn now - ([] - #?(:clj (Instant/now *clock*) - :cljs (new js/Date))) - ([clock] - #?(:cljs (throw (js/Error. "not implemented" {:clock clock})) - :clj (Instant/now ^Clock clock)))) + [] + #?(:clj (Instant/now *clock*) + :cljs (new js/Date))) ;; --- DURATION @@ -325,16 +322,12 @@ :cljs (js/Error. "unsupported type")))))) (defn in-future - ([v] - (plus (now) v)) - ([clock v] - (plus (now clock) v))) + [v] + (plus (now) v)) (defn in-past - ([v] - (minus (now) v)) - ([clock v] - (minus (now clock) v))) + [v] + (minus (now) v)) #?(:clj (defn diff