🐛 Enforce sso before creating organization teams (#12980)

This commit is contained in:
Marina López 2026-11-12 08:35:32 +01:00 committed by GitHub
parent 34f0d12b99
commit df0092fc60
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 133 additions and 9 deletions

View File

@ -13,6 +13,7 @@
[app.common.types.organization :as cto]
[app.common.types.team :as ctt]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
@ -24,7 +25,9 @@
[app.main.repo :as rp]
[app.main.router :as rt]
[app.util.clipboard :as clipboard]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.session-state :as ss]
[app.util.storage :as storage]
[beicon.v2.core :as rx]
[clojure.string :as str]
@ -111,8 +114,8 @@
(f team)))))))
(defn check-and-create-team
"Fetches fresh team data from the server to ensure up-to-date organization
permissions, then shows the team-form modal or a no-permission modal."
"Fetches fresh team data, checks organization permissions and SSO, then
shows the team form or the appropriate access error."
[team-id]
(ptk/reify ::check-and-create-team
ptk/WatchEvent
@ -127,13 +130,44 @@
{:organization-perms organization
:profile-id profile-id
:team-perms (:permissions team)})
true)]
(rx/of (if can-create?
(modal/show :team-form (if in-organization?
{:organization-id (:id organization)
:organization-name (:name organization)}
{}))
(modal/show :no-permission-modal {:type :create-team}))))))))))
true)
team-form-modal (modal/show :team-form (if in-organization?
{:organization-id (:id organization)
:organization-name (:name organization)}
{}))]
(cond
(not can-create?)
(rx/of (modal/show :no-permission-modal {:type :create-team}))
(not in-organization?)
(rx/of team-form-modal)
:else
(let [pending-id (str (uuid/next))
callback-url (dom/append-query-param (rt/get-current-href)
:pending-action-id pending-id)]
(->> (rp/cmd! :check-nitrate-sso {:team-id team-id
:url callback-url})
(rx/mapcat
(fn [{:keys [authorized redirect-uri]}]
(cond
authorized
(rx/of team-form-modal)
redirect-uri
(do
(ss/save-pending-action! pending-id
{:type :create-team-in-organization
:team-id team-id})
(rx/of (rt/nav-raw :uri (str redirect-uri))))
:else
(rx/of (rt/assign-exception
{:type :sso-error
:organization-id (:id organization)
:organization-name (:name organization)
:team-id team-id
:is-dashboard true})))))))))))))))
(defn check-and-delete-team
"Fetches fresh team data from the server to ensure up-to-date organization

View File

@ -18,6 +18,7 @@
[app.main.data.plugins :as dp]
[app.main.data.profile :as dprof]
[app.main.data.project :as dpj]
[app.main.data.team :as dtm]
[app.main.refs :as refs]
[app.main.router :as rt]
[app.main.store :as st]
@ -298,6 +299,8 @@
(st/emit! (dnt/add-team-to-organization {:team-id (:team-id action)
:organization-id (:organization-id action)
:skip-audit? true}))
:create-team-in-organization
(st/emit! (dtm/check-and-create-team (:team-id action)))
nil)))))
(mf/defc dashboard*

View File

@ -5,9 +5,13 @@
(ns frontend-tests.data.team-test
(:require
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.team :as dtm]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.ui.dashboard.team :as dtui]
[app.util.dom :as dom]
[app.util.session-state :as ss]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.async :as async]
@ -27,6 +31,89 @@
:on-next #(swap! emitted conj %))))
(.then (fn [_] {:team-id team-id :events @emitted})))))
(t/deftest ^:async check-and-create-team-redirects-to-sso-and-remembers-the-action
(let [team-id "00000000-0000-4000-8000-000000000001"
organization-id "00000000-0000-4000-8000-000000000002"
profile-id "00000000-0000-4000-8000-000000000003"
dashboard-url "https://penpot.example/#/dashboard-recent?team-id=current-team"
callback-url (str dashboard-url "&pending-action-id=pending-id")
team {:id team-id
:organization {:id organization-id
:name "Product"
:owner-id profile-id}}
requests (atom [])
saved-actions (atom [])
emitted (atom [])]
(await
(mock/with-mocks*
{rp/cmd! (mock/stub
(fn [command params]
(swap! requests conj [command params])
(case command
:get-teams (rx/of [team])
:check-nitrate-sso
(rx/of {:authorized false
:redirect-uri "https://sso.example.test/login"}))))
rt/get-current-href (mock/stub (constantly dashboard-url))
dom/append-query-param (mock/stub (fn [_url _key _value] callback-url))
uuid/next (mock/stub (constantly "pending-id"))
ss/save-pending-action! (mock/stub (fn [& args]
(swap! saved-actions conj args)))
rt/nav-raw (fn [& args] {:nav-raw args})
modal/show (mock/stub (fn [& args] {:modal-show args}))}
(await (async/observe
(ptk/watch (dtm/check-and-create-team team-id)
{:profile {:id profile-id}}
nil)
:on-next #(swap! emitted conj %)))))
(t/is (= [[:get-teams nil]
[:check-nitrate-sso {:team-id team-id :url callback-url}]]
@requests))
(t/is (= [["pending-id" {:type :create-team-in-organization
:team-id team-id}]]
@saved-actions))
(t/is (= {:nav-raw [:uri "https://sso.example.test/login"]}
(last @emitted)))
(t/is (not-any? #(contains? % :modal-show) @emitted))))
(t/deftest ^:async check-and-create-team-shows-form-after-sso-is-authorized
(let [team-id "00000000-0000-4000-8000-000000000001"
organization-id "00000000-0000-4000-8000-000000000002"
profile-id "00000000-0000-4000-8000-000000000003"
dashboard-url "https://penpot.example/#/dashboard-recent?team-id=current-team"
callback-url (str dashboard-url "&pending-action-id=pending-id")
team {:id team-id
:organization {:id organization-id
:name "Product"
:owner-id profile-id}}
requests (atom [])
emitted (atom [])]
(await
(mock/with-mocks*
{rp/cmd! (mock/stub
(fn [command params]
(swap! requests conj [command params])
(case command
:get-teams (rx/of [team])
:check-nitrate-sso (rx/of {:authorized true}))))
rt/get-current-href (mock/stub (constantly dashboard-url))
dom/append-query-param (mock/stub (fn [_url _key _value] callback-url))
uuid/next (mock/stub (constantly "pending-id"))
modal/show (mock/stub (fn [& args] {:modal-show args}))}
(await (async/observe
(ptk/watch (dtm/check-and-create-team team-id)
{:profile {:id profile-id}}
nil)
:on-next #(swap! emitted conj %)))))
(t/is (= [:check-nitrate-sso {:team-id team-id :url callback-url}]
(last @requests)))
(t/is (= {:modal-show [:team-form {:organization-id organization-id
:organization-name "Product"}]}
(last @emitted)))
(t/is (= 2 (count @emitted)))))
(t/deftest ^:async fetch-invitations-clears-the-list-on-permission-error
;; The pending invitations are an admin only list: a member that cannot
;; invite gets an insufficient permissions error. That must not become an