mirror of
https://github.com/penpot/penpot.git
synced 2026-07-21 05:27:50 +00:00
✨ Check for nitrate sso on move team to another organization (#10379)
This commit is contained in:
parent
fa9012e55f
commit
8fa15f240f
@ -805,9 +805,8 @@
|
||||
;; Org SSO flow: state carries :dest-url — exchange the authorization
|
||||
;; code with the OIDC provider to verify authentication actually occurred.
|
||||
(if-let [dest-url (:dest-url state)]
|
||||
(let [team-id (:team-id state)
|
||||
organization-id (:organization-id state)
|
||||
sso (nitrate/call cfg :get-org-sso-by-team {:team-id team-id})
|
||||
(let [organization-id (:organization-id state)
|
||||
sso (nitrate/call cfg :get-org-sso {:organization-id organization-id})
|
||||
provider (prepare-org-sso-provider cfg sso)
|
||||
;; verify token or throw error
|
||||
_info (get-info cfg provider state code)
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.time :as ct]
|
||||
[app.common.types.organization :as cto]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.http.client :as http]
|
||||
[app.http.session :as session]
|
||||
@ -27,6 +28,22 @@
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- join-base-uri
|
||||
[base-uri & segments]
|
||||
(apply u/join (u/ensure-path-slash base-uri) segments))
|
||||
|
||||
(defn- generate-nitrate-uri
|
||||
"Joins relative path segments to the Nitrate backend URI.
|
||||
Segments must not start with `/`"
|
||||
[& segments]
|
||||
(apply join-base-uri (cf/get :nitrate-backend-uri) segments))
|
||||
|
||||
(defn- generate-public-uri
|
||||
"Joins relative path segments to the public backend URI.
|
||||
Segments must not start with `/`"
|
||||
[& segments]
|
||||
(apply join-base-uri (cf/get :public-uri) segments))
|
||||
|
||||
(defn- request-builder
|
||||
[cfg method uri shared-key profile-id request-params]
|
||||
(fn []
|
||||
@ -225,56 +242,49 @@
|
||||
|
||||
(defn- get-team-org-api
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/teams/"
|
||||
team-id)
|
||||
cto/schema:team-with-organization params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri "api/teams/" team-id)
|
||||
cto/schema:team-with-organization params))
|
||||
|
||||
(defn- get-org-membership-api
|
||||
[cfg {:keys [profile-id organization-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/members/"
|
||||
profile-id)
|
||||
schema:profile-org params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"members/"
|
||||
profile-id)
|
||||
schema:profile-org params))
|
||||
|
||||
(defn- get-org-membership-by-team-api
|
||||
[cfg {:keys [profile-id team-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/teams/"
|
||||
team-id
|
||||
"/users/"
|
||||
profile-id)
|
||||
schema:profile-org params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/teams/"
|
||||
team-id
|
||||
"users/"
|
||||
profile-id)
|
||||
schema:profile-org params))
|
||||
|
||||
|
||||
(defn- get-org-summary-api
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/summary")
|
||||
schema:org-summary params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"summary")
|
||||
schema:org-summary params))
|
||||
|
||||
(defn- get-owned-orgs-api
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/users/"
|
||||
profile-id
|
||||
"/owned-organizations")
|
||||
[:vector schema:org-summary]
|
||||
params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/users/"
|
||||
profile-id
|
||||
"owned-organizations")
|
||||
[:vector schema:org-summary]
|
||||
params))
|
||||
|
||||
(def ^:private schema:org-summary-counts
|
||||
[:map
|
||||
@ -288,99 +298,87 @@
|
||||
|
||||
(defn- get-owned-orgs-summary-api
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
||||
orgs (request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/users/"
|
||||
profile-id
|
||||
"/owned-organizations-summary")
|
||||
[:vector schema:org-summary-counts]
|
||||
params)]
|
||||
(let [orgs (request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/users/"
|
||||
profile-id
|
||||
"owned-organizations-summary")
|
||||
[:vector schema:org-summary-counts]
|
||||
params)]
|
||||
(mapv (fn [org]
|
||||
(if-let [logo-id (:logo-id org)]
|
||||
(assoc org :custom-photo (str (cf/get :public-uri) "/assets/by-id/" logo-id))
|
||||
(assoc org :custom-photo (generate-public-uri "assets/by-id/" logo-id))
|
||||
org))
|
||||
orgs)))
|
||||
|
||||
(defn- cleanup-deleted-penpot-user-api
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :post
|
||||
(str baseuri
|
||||
"/api/users/"
|
||||
profile-id
|
||||
"/cleanup-after-deletion")
|
||||
nil params)))
|
||||
(request-to-nitrate cfg :post
|
||||
(generate-nitrate-uri
|
||||
"api/users/"
|
||||
profile-id
|
||||
"cleanup-after-deletion")
|
||||
nil params))
|
||||
|
||||
(defn- set-team-org-api
|
||||
[cfg {:keys [organization-id team-id is-default] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
||||
params (assoc params :request-params {:team-id team-id
|
||||
(let [params (assoc params :request-params {:team-id team-id
|
||||
:is-your-penpot (true? is-default)})
|
||||
team (request-to-nitrate cfg :post
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/add-team")
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"add-team")
|
||||
cto/schema:team-with-organization params)
|
||||
custom-photo (when-let [logo-id (dm/get-in team [:organization :logo-id])]
|
||||
(str (cf/get :public-uri) "/assets/by-id/" logo-id))]
|
||||
(generate-public-uri "assets/by-id/" logo-id))]
|
||||
(cond-> team
|
||||
custom-photo
|
||||
(assoc-in [:organization :custom-photo] custom-photo))))
|
||||
|
||||
(defn- add-profile-to-org-api
|
||||
[cfg {:keys [profile-id organization-id team-id email] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
||||
request-params (cond-> {:user-id profile-id :team-id team-id}
|
||||
(let [request-params (cond-> {:user-id profile-id :team-id team-id}
|
||||
(some? email) (assoc :email email))
|
||||
params (assoc params :request-params request-params)]
|
||||
(request-to-nitrate cfg :post
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/add-user")
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"add-user")
|
||||
schema:profile-org params)))
|
||||
|
||||
(defn- remove-profile-from-org-api
|
||||
[cfg {:keys [profile-id organization-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
||||
params (assoc params :request-params {:user-id profile-id})]
|
||||
(let [params (assoc params :request-params {:user-id profile-id})]
|
||||
(request-to-nitrate cfg :post
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/remove-user")
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"remove-user")
|
||||
nil params)))
|
||||
|
||||
(defn- remove-team-from-org-api
|
||||
[cfg {:keys [team-id organization-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
||||
params (assoc params :request-params {:team-id team-id})]
|
||||
(let [params (assoc params :request-params {:team-id team-id})]
|
||||
(request-to-nitrate cfg :post
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/remove-team")
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"remove-team")
|
||||
nil params)))
|
||||
|
||||
(defn- delete-team-api
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :delete
|
||||
(str baseuri
|
||||
"/api/teams/"
|
||||
team-id)
|
||||
nil params)))
|
||||
(request-to-nitrate cfg :delete
|
||||
(generate-nitrate-uri "api/teams/" team-id)
|
||||
nil params))
|
||||
|
||||
(defn- get-subscription-api
|
||||
[cfg {:keys [profile-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/subscriptions/"
|
||||
profile-id)
|
||||
schema:subscription params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri "api/subscriptions/" profile-id)
|
||||
schema:subscription params))
|
||||
|
||||
(def ^:private schema:subscription-warning
|
||||
[:maybe
|
||||
@ -392,21 +390,16 @@
|
||||
|
||||
(defn- get-subscription-warning-api
|
||||
[cfg {:keys [penpot-id profile-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
||||
penpot-id (or penpot-id profile-id)]
|
||||
(let [penpot-id (or penpot-id profile-id)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/subscription-warning/"
|
||||
penpot-id)
|
||||
(generate-nitrate-uri "api/subscription-warning/" penpot-id)
|
||||
schema:subscription-warning params)))
|
||||
|
||||
(defn- get-connectivity-api
|
||||
[cfg params]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/connectivity")
|
||||
schema:connectivity params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri "api/connectivity")
|
||||
schema:connectivity params))
|
||||
|
||||
(def ^:private schema:redeem-result
|
||||
[:map
|
||||
@ -414,17 +407,16 @@
|
||||
|
||||
(defn- get-org-permissions-api
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/permissions")
|
||||
[:map
|
||||
[:organization-id ::sm/uuid]
|
||||
[:owner-id ::sm/uuid]
|
||||
[:permissions [:map-of :keyword :string]]]
|
||||
params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"permissions")
|
||||
[:map
|
||||
[:organization-id ::sm/uuid]
|
||||
[:owner-id ::sm/uuid]
|
||||
[:permissions [:map-of :keyword :string]]]
|
||||
params))
|
||||
|
||||
(def ^:private schema:nitrate-sso
|
||||
[:map
|
||||
@ -437,35 +429,40 @@
|
||||
[:issuer [:maybe :string]]
|
||||
[:scopes [:maybe [::sm/set ::sm/text]]]])
|
||||
|
||||
(defn- get-org-sso-api
|
||||
"Fetches the SSO configuration for an organization from Nitrate."
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"sso")
|
||||
schema:nitrate-sso
|
||||
params))
|
||||
|
||||
(defn- get-org-sso-by-team-api
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/teams/"
|
||||
team-id
|
||||
"/sso")
|
||||
schema:nitrate-sso
|
||||
params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri "api/teams/" team-id "sso")
|
||||
schema:nitrate-sso
|
||||
params))
|
||||
|
||||
(defn- get-org-members-api
|
||||
[cfg {:keys [organization-id] :as params}]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :get
|
||||
(str baseuri
|
||||
"/api/organizations/"
|
||||
organization-id
|
||||
"/members-list")
|
||||
[:vector ::sm/uuid]
|
||||
params)))
|
||||
(request-to-nitrate cfg :get
|
||||
(generate-nitrate-uri
|
||||
"api/organizations/"
|
||||
organization-id
|
||||
"members-list")
|
||||
[:vector ::sm/uuid]
|
||||
params))
|
||||
|
||||
(defn- redeem-activation-code-api
|
||||
[cfg params]
|
||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||
(request-to-nitrate cfg :post
|
||||
(str baseuri "/api/activation-codes/redeem")
|
||||
schema:redeem-result
|
||||
(assoc params :throw-on-error? true))))
|
||||
(request-to-nitrate cfg :post
|
||||
(generate-nitrate-uri "api/activation-codes/redeem")
|
||||
schema:redeem-result
|
||||
(assoc params :throw-on-error? true)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INITIALIZATION
|
||||
@ -487,6 +484,7 @@
|
||||
:remove-profile-from-org (partial remove-profile-from-org-api cfg)
|
||||
:get-org-permissions (partial get-org-permissions-api cfg)
|
||||
:get-org-sso-by-team (partial get-org-sso-by-team-api cfg)
|
||||
:get-org-sso (partial get-org-sso-api cfg)
|
||||
:delete-team (partial delete-team-api cfg)
|
||||
:remove-team-from-org (partial remove-team-from-org-api cfg)
|
||||
:get-subscription (partial get-subscription-api cfg)
|
||||
@ -499,11 +497,14 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn sso-session-authorized?
|
||||
"Fetches the org-SSO config for the given team and checks whether
|
||||
the HTTP request has a valid session entry for it. Returns a map
|
||||
"Fetches the org-SSO config for the given organization or team and checks
|
||||
whether the HTTP request has a valid session entry for it. Returns a map
|
||||
with :authorized and :sso keys."
|
||||
[cfg team-id request]
|
||||
(let [session (session/get-session request) sso (call cfg :get-org-sso-by-team {:team-id team-id})]
|
||||
[cfg organization-id team-id request]
|
||||
(let [session (session/get-session request)
|
||||
sso (if organization-id
|
||||
(call cfg :get-org-sso {:organization-id organization-id})
|
||||
(call cfg :get-org-sso-by-team {:team-id team-id}))]
|
||||
(if-not (:active sso)
|
||||
{:authorized true :sso sso}
|
||||
(if (or (:issuer sso) (:base-url sso))
|
||||
@ -549,7 +550,7 @@
|
||||
(if (some? org)
|
||||
(-> (cto/apply-organization team (assoc org :custom-photo
|
||||
(when-let [logo-id (:logo-id org)]
|
||||
(str (cf/get :public-uri) "/assets/by-id/" logo-id))))
|
||||
(generate-public-uri "assets/by-id/" logo-id))))
|
||||
(assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-org)))))
|
||||
team))
|
||||
(catch Throwable cause
|
||||
|
||||
@ -223,15 +223,16 @@
|
||||
(defn- wrap-nitrate-sso
|
||||
"Enforce Nitrate organization SSO authentication for RPC handlers.
|
||||
|
||||
Resolves the team context from request params using priority order:
|
||||
1. Explicit :team-id param
|
||||
2. Explicit :project-id param → lookup project.team_id
|
||||
3. Explicit :file-id param → lookup file's team via join
|
||||
4. :id param dispatched by ::rpc/id-type metadata (:team, :project, or :file)
|
||||
Resolves the organization/team context from request params using priority order:
|
||||
1. Explicit :organization-id param
|
||||
2. Explicit :team-id param
|
||||
3. Explicit :project-id param -> lookup project.team_id
|
||||
4. Explicit :file-id param -> lookup file's team via join
|
||||
5. :id param dispatched by ::rpc/id-type metadata (:team, :project, or :file)
|
||||
|
||||
Once team-id is resolved, checks if the user is authorized within that org's SSO
|
||||
session using nitrate/sso-session-authorized?. Results are cached by [profile-id cache-ref]
|
||||
for 15 minutes to avoid repeated lookups.
|
||||
Once the context is resolved, checks if the user is authorized within that org's
|
||||
SSO session using nitrate/sso-session-authorized?. Authorized results are cached
|
||||
by [profile-id cache-ref] for 15 minutes to avoid repeated lookups.
|
||||
|
||||
Only activates when:
|
||||
- Nitrate flag is enabled
|
||||
@ -245,27 +246,31 @@
|
||||
(::nitrate/sso mdata true))
|
||||
(fn [cfg params]
|
||||
;; Resolve team/project/file from explicit keys or from :id via metadata
|
||||
(let [id-type (::id-type mdata)
|
||||
id (uuid/coerce (:id params))
|
||||
team-id (or (uuid/coerce (:team-id params))
|
||||
(when (= id-type :team) id))
|
||||
project-id (or (uuid/coerce (:project-id params))
|
||||
(when (= id-type :project) id))
|
||||
file-id (or (uuid/coerce (:file-id params))
|
||||
(when (= id-type :file) id))]
|
||||
(if (or team-id project-id file-id)
|
||||
(let [cache-ref (or team-id project-id file-id)
|
||||
(let [organization-id (uuid/coerce (:organization-id params))
|
||||
id-type (::id-type mdata)
|
||||
id (uuid/coerce (:id params))
|
||||
team-id (or (uuid/coerce (:team-id params))
|
||||
(when (= id-type :team) id))
|
||||
project-id (or (uuid/coerce (:project-id params))
|
||||
(when (= id-type :project) id))
|
||||
file-id (or (uuid/coerce (:file-id params))
|
||||
(when (= id-type :file) id))]
|
||||
(if (or organization-id team-id project-id file-id)
|
||||
(let [cache-ref (or organization-id team-id project-id file-id)
|
||||
profile-id (::profile-id params)
|
||||
cache-key [profile-id cache-ref]
|
||||
cached (cache/get org-sso-auth-cache cache-key)
|
||||
result (if (some? cached)
|
||||
cached
|
||||
(let [team-id (or team-id
|
||||
(when project-id
|
||||
(:team-id (db/get-by-id cfg :project project-id {:columns [:id :team-id]})))
|
||||
(:id (teams/get-team-for-file cfg file-id)))
|
||||
(let [team-id (when-not organization-id
|
||||
(or team-id
|
||||
(when project-id
|
||||
(:team-id (db/get-by-id cfg :project project-id {:columns [:id :team-id]})))
|
||||
(:id (teams/get-team-for-file cfg file-id))))
|
||||
request (-> (meta params) (get ::http/request))
|
||||
{:keys [authorized sso]} (nitrate/sso-session-authorized? cfg team-id request)
|
||||
{:keys [authorized sso]} (if organization-id
|
||||
(nitrate/sso-session-authorized? cfg organization-id nil request)
|
||||
(nitrate/sso-session-authorized? cfg nil team-id request))
|
||||
entry {:authorized authorized
|
||||
:organization-id (:organization-id sso)}]
|
||||
(when authorized
|
||||
|
||||
@ -444,7 +444,7 @@
|
||||
::doc/added "2.17"
|
||||
::sm/params schema:add-team-to-organization
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id team-id organization-id]}]
|
||||
[cfg {:keys [::rpc/profile-id team-id organization-id]}]
|
||||
|
||||
(assert-is-owner cfg profile-id team-id)
|
||||
(assert-not-default-team cfg team-id)
|
||||
@ -621,13 +621,17 @@
|
||||
|
||||
|
||||
(def ^:private schema:check-nitrate-sso
|
||||
[:map {:title "AuthSsoParams"}
|
||||
[:team-id ::sm/uuid]
|
||||
[:url ::sm/uri]])
|
||||
[:and
|
||||
[:map {:title "CheckNitrateSsoParams"}
|
||||
[:team-id {:optional true} ::sm/uuid]
|
||||
[:organization-id {:optional true} ::sm/uuid]
|
||||
[:url ::sm/uri]]
|
||||
[::sm/contains-any #{:team-id :organization-id}]])
|
||||
|
||||
(sv/defmethod ::check-nitrate-sso
|
||||
"Check if a user needs to login into the organization SSO.
|
||||
Returns {:authorized true} when SSO is not active for the team.
|
||||
Accepts either team-id (to look up the org via the team) or organization-id directly.
|
||||
Returns {:authorized true} when SSO is not active.
|
||||
Returns {:authorized false :redirect-uri <url>} when SSO is active;
|
||||
the client must redirect there. The OIDC provider itself handles
|
||||
re-authentication transparently if the user already has an active SSO session."
|
||||
@ -635,22 +639,21 @@
|
||||
::doc/added "2.19"
|
||||
::sm/params schema:check-nitrate-sso
|
||||
::nitrate/sso false}
|
||||
[cfg {:keys [team-id url] :as params}]
|
||||
[cfg {:keys [team-id organization-id url] :as params}]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(let [request (rph/get-request params)
|
||||
{:keys [authorized sso]} (nitrate/sso-session-authorized? cfg team-id request)]
|
||||
(let [request (rph/get-request params)
|
||||
{:keys [authorized sso]} (nitrate/sso-session-authorized? cfg organization-id team-id request)]
|
||||
(if authorized
|
||||
{:authorized true}
|
||||
(if-let [issuer (or (:issuer sso) (:base-url sso))]
|
||||
(let [oidc-provider (oidc/prepare-org-sso-provider cfg sso)
|
||||
organization-id (:organization-id sso)
|
||||
state-token (tokens/generate cfg {:iss "oidc"
|
||||
:dest-url url
|
||||
:team-id team-id
|
||||
:organization-id organization-id
|
||||
:issuer issuer
|
||||
:exp (ct/in-future "4h")})
|
||||
redirect-uri (oidc/build-auth-redirect-uri oidc-provider state-token)]
|
||||
(let [oidc-provider (oidc/prepare-org-sso-provider cfg sso)
|
||||
org-id (or organization-id (:organization-id sso))
|
||||
state-token (tokens/generate cfg {:iss "oidc"
|
||||
:dest-url url
|
||||
:organization-id org-id
|
||||
:issuer issuer
|
||||
:exp (ct/in-future "4h")})
|
||||
redirect-uri (oidc/build-auth-redirect-uri oidc-provider state-token)]
|
||||
{:authorized false
|
||||
:redirect-uri redirect-uri})
|
||||
{:authorized false
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.nitrate-permissions :as nitrate-perms]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.modal :as modal]
|
||||
@ -11,10 +12,11 @@
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[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]
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:private nitrate-entry-active-key ::nitrate-entry-active)
|
||||
@ -90,28 +92,12 @@
|
||||
(def nitrate-checkout-finish-error-token "nitrate-checkout-finish-error")
|
||||
(def nitrate-checkout-cancelled-token "nitrate-checkout-cancelled")
|
||||
|
||||
(defn- append-query-param
|
||||
[url key value]
|
||||
(let [assoc-q (fn [u]
|
||||
(update u :query
|
||||
(fn [q]
|
||||
(-> (u/query-string->map (or q ""))
|
||||
(assoc (name key) value)
|
||||
u/map->query-string))))
|
||||
parsed (u/uri url)
|
||||
fragment (:fragment parsed)]
|
||||
(if (str/blank? fragment)
|
||||
(str (assoc-q parsed))
|
||||
(-> parsed
|
||||
(assoc :fragment (str (assoc-q (u/parse fragment))))
|
||||
str))))
|
||||
|
||||
(defn build-nitrate-callback-urls
|
||||
"Build the success/error/cancel callback URLs from a base URL by appending
|
||||
a `subscription` query param identifying the outcome."
|
||||
[base-url]
|
||||
(let [build (fn [token]
|
||||
(append-query-param base-url :subscription token))]
|
||||
(dom/append-query-param base-url :subscription token))]
|
||||
{:success-callback (build "subscribed-to-penpot-nitrate")
|
||||
:error-callback (build nitrate-checkout-error-token)
|
||||
:finish-error-callback (build nitrate-checkout-finish-error-token)
|
||||
@ -261,15 +247,32 @@
|
||||
(modal/show :no-permission-modal {:type :no-orgs-change}))))))))))
|
||||
|
||||
|
||||
(defn add-team-to-org
|
||||
[{:keys [team-id organization-id] :as params}]
|
||||
(ptk/reify ::add-team-to-org
|
||||
(defn add-team-to-organization
|
||||
"Adds a team to an organization after checking whether the target
|
||||
organization requires a fresh Nitrate SSO session. When SSO is required,
|
||||
stores the pending action and redirects to the provider so the dashboard
|
||||
can resume the operation after the callback."
|
||||
[{:keys [team-id organization-id]}]
|
||||
(ptk/reify ::add-team-to-organization
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! ::add-team-to-organization {:team-id team-id :organization-id organization-id})
|
||||
(rx/mapcat
|
||||
(fn [_]
|
||||
(rx/of (modal/hide))))))))
|
||||
(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 {:organization-id organization-id :url callback-url})
|
||||
(rx/mapcat
|
||||
(fn [{:keys [authorized redirect-uri]}]
|
||||
(if authorized
|
||||
(->> (rp/cmd! ::add-team-to-organization {:team-id team-id :organization-id organization-id})
|
||||
(rx/map (fn [_] (modal/hide))))
|
||||
(if redirect-uri
|
||||
(do
|
||||
(ss/save-pending-action! pending-id {:type :add-team-to-organization
|
||||
:team-id team-id
|
||||
:organization-id organization-id})
|
||||
(rx/of (rt/nav-raw :uri (str redirect-uri))))
|
||||
(rx/empty))))))))))
|
||||
|
||||
|
||||
(defn- fetch-orgs-allowed
|
||||
"Returns an rx observable of an `orgs-allowed` map (org-id -> boolean).
|
||||
@ -286,12 +289,12 @@
|
||||
(merge (into {} (map (fn [org] [(:id org) true])) add-anybody-orgs)
|
||||
checked-orgs)))))))
|
||||
|
||||
(defn show-add-team-to-org-modal
|
||||
(defn show-add-team-to-organization-modal
|
||||
"Fetches fresh team/org data, then shows the add-to-org modal
|
||||
restricted to orgs where the user has permission, or the no-permission
|
||||
modal if none qualify."
|
||||
[{:keys [team-id]}]
|
||||
(ptk/reify ::show-add-team-to-org-modal
|
||||
(ptk/reify ::show-add-team-to-organization-modal
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile-id (dm/get-in state [:profile :id])]
|
||||
@ -306,8 +309,8 @@
|
||||
(or (= perm "any") is-own?))) all-orgs)
|
||||
team (first (filter #(= (:id %) team-id) teams))
|
||||
on-confirm (fn [organization-id]
|
||||
(st/emit! (add-team-to-org {:team-id team-id
|
||||
:organization-id organization-id})))
|
||||
(st/emit! (add-team-to-organization {:team-id team-id
|
||||
:organization-id organization-id})))
|
||||
show-select-modal
|
||||
(fn [orgs-allowed]
|
||||
(let [has-filtered? (< (count orgs) (count all-orgs))
|
||||
@ -377,8 +380,8 @@
|
||||
orgs (filter can-create? orgs-by-move)
|
||||
selectable-orgs (remove #(= current-org-id (:id %)) orgs)
|
||||
on-confirm (fn [organization-id]
|
||||
(st/emit! (add-team-to-org {:team-id team-id
|
||||
:organization-id organization-id})))]
|
||||
(st/emit! (add-team-to-organization {:team-id team-id
|
||||
:organization-id organization-id})))]
|
||||
(if (empty? selectable-orgs)
|
||||
(rx/of (dt/teams-fetched teams)
|
||||
(modal/show :no-permission-modal {:type :no-orgs-change}))
|
||||
|
||||
@ -229,11 +229,12 @@
|
||||
:dashboard-settings
|
||||
:dashboard-deleted)
|
||||
(let [params (get params :query)
|
||||
team-id (some-> params :team-id uuid/parse*)
|
||||
project-id (some-> params :project-id uuid/parse*)
|
||||
search-term (some-> params :search-term)
|
||||
plugin-url (some-> params :plugin)
|
||||
template (some-> params :template)]
|
||||
team-id (some-> params :team-id uuid/parse*)
|
||||
project-id (some-> params :project-id uuid/parse*)
|
||||
search-term (some-> params :search-term)
|
||||
plugin-url (some-> params :plugin)
|
||||
template (some-> params :template)
|
||||
pending-action-id (some-> params :pending-action-id uuid/parse*)]
|
||||
[:?
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.5"}]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
@ -257,7 +258,8 @@
|
||||
:search-term search-term
|
||||
:plugin-url plugin-url
|
||||
:project-id project-id
|
||||
:template template}]]])
|
||||
:template template
|
||||
:pending-action-id pending-action-id}]]])
|
||||
|
||||
:workspace
|
||||
(let [params (get params :query)
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.session-state :as ss]
|
||||
[app.util.storage :as storage]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
@ -270,8 +271,22 @@
|
||||
(st/emit! (dprof/update-profile-props {:onboarding-viewed true})
|
||||
(dnt/show-nitrate-popup :nitrate-form)))))
|
||||
|
||||
(defn- use-pending-action
|
||||
"Consumes a pending dashboard action from session storage and resumes it"
|
||||
[pending-action-id]
|
||||
(mf/with-effect [pending-action-id]
|
||||
(when (some? pending-action-id)
|
||||
(dom/replace-history-state!
|
||||
(dom/remove-query-param (rt/get-current-href) :pending-action-id))
|
||||
(when-let [action (ss/consume-pending-action! (str pending-action-id))]
|
||||
(case (:type action)
|
||||
:add-team-to-organization
|
||||
(st/emit! (dnt/add-team-to-organization {:team-id (:team-id action)
|
||||
:organization-id (:organization-id action)}))
|
||||
nil)))))
|
||||
|
||||
(mf/defc dashboard*
|
||||
[{:keys [profile project-id team-id search-term plugin-url template section]}]
|
||||
[{:keys [profile project-id team-id search-term plugin-url template section pending-action-id]}]
|
||||
(let [team (mf/deref refs/team)
|
||||
projects (mf/deref refs/projects)
|
||||
|
||||
@ -309,6 +324,7 @@
|
||||
(use-plugin-register plugin-url team-id (:id default-project))
|
||||
(use-templates-import can-edit? template default-project)
|
||||
(use-nitrate-entry-popup)
|
||||
(use-pending-action pending-action-id)
|
||||
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:> modal-container*]
|
||||
|
||||
@ -1559,11 +1559,11 @@
|
||||
(fn []
|
||||
(st/emit! (dnt/show-remove-team-from-org-modal {:team-id (:id team)}))))
|
||||
|
||||
on-add-team-to-org
|
||||
on-add-team-to-organization
|
||||
(mf/use-fn
|
||||
(mf/deps team)
|
||||
(fn []
|
||||
(st/emit! (dnt/show-add-team-to-org-modal {:team-id (:id team)}))))
|
||||
(st/emit! (dnt/show-add-team-to-organization-modal {:team-id (:id team)}))))
|
||||
|
||||
on-change-team-org
|
||||
(mf/use-fn
|
||||
@ -1640,7 +1640,7 @@
|
||||
(when can-add-to-organization?
|
||||
[:div {:class (stl/css :block-content)}
|
||||
[:span {:class (stl/css :block-text)}
|
||||
[:a {:on-click on-add-team-to-org} (tr "dashboard.team-organization.add")]]])]))])
|
||||
[:a {:on-click on-add-team-to-organization} (tr "dashboard.team-organization.add")]]])]))])
|
||||
|
||||
[:div {:class (stl/css :block)}
|
||||
[:div {:class (stl/css :block-label)}
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
[app.common.geom.rect :as grc]
|
||||
[app.common.logging :as log]
|
||||
[app.common.media :as cm]
|
||||
[app.common.uri :as u]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.object :as obj]
|
||||
[app.util.webapi :as wapi]
|
||||
@ -855,7 +856,42 @@
|
||||
|
||||
(defn browser-back
|
||||
[]
|
||||
(.back (.-history js/window)))
|
||||
(.back (.-history globals/window)))
|
||||
|
||||
(defn replace-history-state!
|
||||
"Replace the current browser history entry URL without triggering navigation."
|
||||
[url]
|
||||
(.replaceState (.-history globals/window) nil "" url))
|
||||
|
||||
(defn- update-query-params
|
||||
"Apply `f` to the query-params map of `url`, returning the updated URL string.
|
||||
Handles both plain query strings and fragment-based (hash) URLs."
|
||||
[url f]
|
||||
(let [transform (fn [parsed]
|
||||
(update parsed :query
|
||||
(fn [q]
|
||||
(-> (u/query-string->map (or q ""))
|
||||
f
|
||||
u/map->query-string))))
|
||||
parsed (u/uri url)
|
||||
fragment (:fragment parsed)]
|
||||
(if (str/blank? fragment)
|
||||
(str (transform parsed))
|
||||
(-> parsed
|
||||
(assoc :fragment (str (transform (u/parse fragment))))
|
||||
str))))
|
||||
|
||||
(defn append-query-param
|
||||
"Return a new URL string with the given query parameter added or replaced.
|
||||
Handles both plain query strings and fragment-based (hash) URLs."
|
||||
[url key value]
|
||||
(update-query-params url #(assoc % key value)))
|
||||
|
||||
(defn remove-query-param
|
||||
"Return a new URL string with the given query parameter removed.
|
||||
Handles both plain query strings and fragment-based (hash) URLs."
|
||||
[url key]
|
||||
(update-query-params url #(dissoc % key)))
|
||||
|
||||
(defn reload-current-window
|
||||
([]
|
||||
|
||||
29
frontend/src/app/util/session_state.cljs
Normal file
29
frontend/src/app/util/session_state.cljs
Normal file
@ -0,0 +1,29 @@
|
||||
;; 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 app.util.session-state
|
||||
"Helpers for persisting transient state in browser session storage
|
||||
so it survives cross-origin redirects."
|
||||
(:require
|
||||
[app.util.storage :as storage]))
|
||||
|
||||
(def ^:private pending-actions-key ::pending-actions)
|
||||
|
||||
(defn save-pending-action!
|
||||
"Persist an action map under `id` in the browser session."
|
||||
[id data]
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session update pending-actions-key assoc id data)))
|
||||
|
||||
(defn consume-pending-action!
|
||||
"Read and remove the pending action stored under `id`.
|
||||
Returns nil if not found."
|
||||
[id]
|
||||
(let [action (get-in @storage/session [pending-actions-key id])]
|
||||
(when (some? action)
|
||||
(binding [storage/*sync* true]
|
||||
(swap! storage/session update pending-actions-key dissoc id)))
|
||||
action))
|
||||
Loading…
x
Reference in New Issue
Block a user