🐛 Cache Nitrate SSO checks during navigation (#11209)

This commit is contained in:
Marina López 2026-08-11 13:30:04 +02:00 committed by GitHub
parent 69ef7e86cd
commit 02c31e7348
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 125 additions and 14 deletions

View File

@ -7,6 +7,7 @@
(ns app.main.ui.routes
(:require
[app.common.data.macros :as dm]
[app.common.time :as ct]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -21,6 +22,12 @@
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(def ^:private sso-authorization-max-age-ms
(* 5 60 1000))
(defonce ^:private sso-authorization-cache
(atom {}))
(def routes
[["/auth"
["/login" :auth-login]
@ -102,26 +109,43 @@
"Authorization filter for dashboard and workspace routes.
Checks if the team being navigated to has an organization with SSO
active. If so, calls :check-nitrate-sso and either proceeds with navigation
or redirects to the SSO provider URL."
or redirects to the SSO provider URL. Successful checks are cached for five
minutes per profile and team; redirect results are never cached."
[match send-event-info? url]
(let [route-name (name (get-in match [:data :name]))
relevant? (and (contains? cf/flags :admin-console)
(or (str/starts-with? route-name "dashboard")
(str/starts-with? route-name "workspace")))
team-id-str (when relevant?
(or (get-in match [:query-params :team-id])
(get-in match [:params :path :team-id])))
team-id (some-> team-id-str uuid/parse*)]
(if (some? team-id)
(let [route-name (name (get-in match [:data :name]))
relevant? (and (contains? cf/flags :admin-console)
(or (str/starts-with? route-name "dashboard")
(str/starts-with? route-name "workspace")))
team-id-str (when relevant?
(or (get-in match [:query-params :team-id])
(get-in match [:params :path :team-id])))
team-id (some-> team-id-str uuid/parse*)
profile-id (get-in @st/state [:profile :id])
cache-key [profile-id team-id]
authorized-at (get @sso-authorization-cache cache-key)
cache-valid? (and (some? authorized-at)
(< (ct/diff-ms authorized-at (ct/now))
sso-authorization-max-age-ms))
navigate #(st/emit! (rt/navigated match send-event-info?))]
(cond
(nil? team-id)
(navigate)
cache-valid?
(navigate)
:else
(->> (rp/cmd! :check-nitrate-sso {:team-id team-id :url url})
(rx/subs!
(fn [{:keys [authorized redirect-uri]}]
(if authorized
(st/emit! (rt/navigated match send-event-info?))
(when redirect-uri (st/emit! (rt/nav-raw :uri (str redirect-uri))))))
(do
(swap! sso-authorization-cache assoc cache-key (ct/now))
(navigate))
(when redirect-uri
(st/emit! (rt/nav-raw :uri (str redirect-uri))))))
(fn [cause]
(errors/on-error cause))))
(st/emit! (rt/navigated match send-event-info?)))))
(errors/on-error cause)))))))
(defn- handle-sso-error-and-navigate
"Check if the current route has an SSO error marker. If so, assign an

View File

@ -72,6 +72,7 @@
[frontend-tests.ui.gradient-handlers-test]
[frontend-tests.ui.layout-container-multiple-test]
[frontend-tests.ui.measures-menu-props-test]
[frontend-tests.ui.routes-test]
[frontend-tests.ui.settings-password-schema-test]
[frontend-tests.ui.settings-shortcuts-test]
[frontend-tests.util-clipboard-test]
@ -163,6 +164,7 @@
'frontend-tests.ui.gradient-handlers-test
'frontend-tests.ui.layout-container-multiple-test
'frontend-tests.ui.measures-menu-props-test
'frontend-tests.ui.routes-test
'frontend-tests.render-dimensions-test
'frontend-tests.text-editor-paste-guard-test
'frontend-tests.ui.settings-password-schema-test

View File

@ -0,0 +1,85 @@
;; 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 Sucursal en España SL
(ns frontend-tests.ui.routes-test
(:require
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.routes :as routes]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]))
(defn- workspace-match
[team-id]
{:data {:name :workspace}
:params {:path {}}
:query-params {:team-id (str team-id)}})
(t/deftest sso-check-is-cached-for-five-minutes
(let [team-id (uuid/next)
match (workspace-match team-id)
now (atom (ct/inst "2026-08-11T10:00:00Z"))
rpc-calls (atom 0)
events (atom [])]
(with-redefs [cf/flags (conj cf/flags :admin-console)
ct/now (mock/stub (fn [] @now))
rp/cmd! (mock/stub
(fn [command params]
(t/is (= :check-nitrate-sso command))
(t/is (= team-id (:team-id params)))
(swap! rpc-calls inc)
(rx/of {:authorized true})))
st/emit! (mock/stub
(fn [& emitted]
(swap! events into emitted)))]
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
(reset! now (ct/plus @now #js {:minutes 4 :seconds 59}))
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
(t/is (= 1 @rpc-calls))
(t/is (= 2 (count @events))))))
(t/deftest sso-check-is-refreshed-after-five-minutes
(let [team-id (uuid/next)
match (workspace-match team-id)
now (atom (ct/inst "2026-08-11T10:00:00Z"))
rpc-calls (atom 0)]
(with-redefs [cf/flags (conj cf/flags :admin-console)
ct/now (mock/stub (fn [] @now))
rp/cmd! (mock/stub
(fn [_ _]
(swap! rpc-calls inc)
(rx/of {:authorized true})))
st/emit! mock/noop]
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
(reset! now (ct/plus @now #js {:minutes 5}))
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
(t/is (= 2 @rpc-calls)))))
(t/deftest sso-redirect-result-is-not-cached
(let [team-id (uuid/next)
match (workspace-match team-id)
rpc-calls (atom 0)
events (atom [])]
(with-redefs [cf/flags (conj cf/flags :admin-console)
rp/cmd! (mock/stub
(fn [_ _]
(swap! rpc-calls inc)
(rx/of {:authorized false
:redirect-uri "https://idp.example.com/authorize"})))
st/emit! (mock/stub
(fn [& emitted]
(swap! events into emitted)))]
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
(#'routes/check-sso-and-navigate match true "https://penpot.example.com/#/workspace")
(t/is (= 2 @rpc-calls))
(t/is (= 2 (count @events))))))