mirror of
https://github.com/penpot/penpot.git
synced 2026-08-07 21:38:48 +00:00
🐛 WIP
This commit is contained in:
parent
229d24e8f2
commit
d2d9afea44
@ -650,6 +650,20 @@
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
(redirect-response uri))))
|
||||
|
||||
(defn- redirect-with-organization-sso-error
|
||||
[{:keys [dest-url organization-id]}]
|
||||
(let [uri (u/uri (or dest-url (cf/get :public-uri)))
|
||||
params (d/without-nils {:sso-error true :organization-id organization-id})
|
||||
qs (u/map->query-string params)
|
||||
fragment (or (:fragment uri) "")
|
||||
parsed-frag (when-not (str/blank? fragment) (u/parse fragment))
|
||||
new-fragment (if parsed-frag
|
||||
(let [existing-query (or (:query parsed-frag) "")
|
||||
combined (if (str/blank? existing-query) qs (str existing-query "&" qs))]
|
||||
(str (assoc parsed-frag :query combined)))
|
||||
(str "?" qs))]
|
||||
(redirect-response (assoc uri :fragment new-fragment))))
|
||||
|
||||
(defn- redirect-to-register
|
||||
[cfg info provider]
|
||||
(let [info (assoc info
|
||||
@ -854,9 +868,25 @@
|
||||
:body (u/map->query-string params)}
|
||||
response (http/req cfg req {:skip-ssrf-check? (:skip-ssrf-check? provider)})]
|
||||
(cond
|
||||
(token-endpoint-valid-client-error? response) true
|
||||
(token-endpoint-invalid-client-error? response) false
|
||||
:else false)))
|
||||
(token-endpoint-valid-client-error? response)
|
||||
true
|
||||
|
||||
(token-endpoint-invalid-client-error? response)
|
||||
(do
|
||||
(l/warn :hint "organization SSO client credentials rejected by the token endpoint"
|
||||
:token-uri (:token-uri provider)
|
||||
:status (:status response)
|
||||
:error (token-endpoint-error response)
|
||||
:error-description (token-endpoint-error-description response))
|
||||
false)
|
||||
|
||||
:else
|
||||
(do
|
||||
(l/warn :hint "unexpected token endpoint response while probing organization SSO client credentials"
|
||||
:token-uri (:token-uri provider)
|
||||
:status (:status response)
|
||||
:body (:body response))
|
||||
false))))
|
||||
|
||||
(defn is-organization-sso-config-valid?
|
||||
"Return true when the SSO config can be discovered, can build a login URL,
|
||||
@ -867,8 +897,15 @@
|
||||
(let [provider (prepare-organization-sso-provider cfg sso)]
|
||||
(and (build-organization-sso-auth-redirect-uri cfg sso :provider provider)
|
||||
(probe-organization-sso-client-credentials cfg provider)))
|
||||
false)
|
||||
(catch Throwable _ false)))
|
||||
(do
|
||||
(l/warn :hint "organization SSO config is missing a valid issuer"
|
||||
:issuer (:issuer sso))
|
||||
false))
|
||||
(catch Throwable cause
|
||||
(l/warn :hint "organization SSO config validation failed"
|
||||
:issuer (:issuer sso)
|
||||
:cause cause)
|
||||
false)))
|
||||
|
||||
(defn- auth-handler
|
||||
[cfg {:keys [params] :as request}]
|
||||
@ -899,17 +936,32 @@
|
||||
;; Organization 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 [organization-id (:organization-id state)
|
||||
sso (nitrate/call cfg :get-organization-sso {:organization-id organization-id})
|
||||
provider (prepare-organization-sso-provider cfg sso)
|
||||
info (get-info cfg provider state code)
|
||||
session (session/get-session request)
|
||||
exp (or (:sso-token-exp info) (ct/in-future {:hours 48}))]
|
||||
(when (and session organization-id)
|
||||
(let [props (-> (or (:props session) {})
|
||||
(update :sso assoc organization-id exp))]
|
||||
(session/update-session (::session/manager cfg) (assoc session :props props))))
|
||||
(redirect-response dest-url))
|
||||
(try
|
||||
(let [organization-id (:organization-id state)
|
||||
sso (nitrate/call cfg :get-organization-sso {:organization-id organization-id})
|
||||
provider (prepare-organization-sso-provider cfg sso)
|
||||
info (get-info cfg provider state code)
|
||||
session (session/get-session request)
|
||||
exp (or (:sso-token-exp info) (ct/in-future {:hours 48}))]
|
||||
(when (and session organization-id)
|
||||
(let [props (-> (or (:props session) {})
|
||||
(update :sso assoc organization-id exp))]
|
||||
(session/update-session (::session/manager cfg) (assoc session :props props))))
|
||||
(redirect-response dest-url))
|
||||
(catch Throwable cause
|
||||
(let [{:keys [code]} (ex-data cause)]
|
||||
(binding [l/*context* (errors/request->context request)]
|
||||
(if (some? code)
|
||||
(l/warn :hint "organization sso callback failed"
|
||||
:code code
|
||||
:message (ex-message cause)
|
||||
:organization-id (:organization-id state))
|
||||
(l/err :hint "unexpected error on organization sso callback"
|
||||
:organization-id (:organization-id state)
|
||||
:cause cause))))
|
||||
(redirect-with-organization-sso-error
|
||||
{:dest-url dest-url
|
||||
:organization-id (:organization-id state)})))
|
||||
|
||||
(let [provider (resolve-provider cfg state)
|
||||
info (get-info cfg provider state code)
|
||||
|
||||
@ -351,6 +351,21 @@
|
||||
(rx/empty)))))))))))
|
||||
|
||||
|
||||
(defn retry-organization-sso
|
||||
"Retries the organization SSO login flow after a failed attempt, reusing
|
||||
the same check-nitrate-sso RPC used elsewhere to move the user through
|
||||
the organization's identity provider. Falls back to navigating straight
|
||||
to `dest-url` when no fresh SSO redirect is needed or available."
|
||||
[{:keys [organization-id dest-url]}]
|
||||
(ptk/reify ::retry-organization-sso
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/cmd! :check-nitrate-sso {:organization-id organization-id :url dest-url})
|
||||
(rx/map (fn [{:keys [redirect-uri]}]
|
||||
(rt/nav-raw :uri (or redirect-uri dest-url))))
|
||||
(rx/catch (fn [_]
|
||||
(rx/of (rt/nav-raw :uri dest-url))))))))
|
||||
|
||||
(defn- fetch-organizations-allowed
|
||||
"Returns an rx observable of an `organizations-allowed` map (organization-id -> boolean).
|
||||
Organizations where :add-anybody-to-team is permitted are pre-approved;
|
||||
|
||||
@ -237,57 +237,65 @@
|
||||
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]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:> app.main.ui.onboarding.team-choice/onboarding-team-modal*]
|
||||
pending-action-id (some-> params :pending-action-id uuid/parse*)
|
||||
sso-error? (some? (:sso-error params))
|
||||
organization-id (some-> params :organization-id uuid/parse*)]
|
||||
(if sso-error?
|
||||
[:> static/sso-error* {:organization-id organization-id :profile profile :is-dashboard true}]
|
||||
[:?
|
||||
#_[:& app.main.ui.releases/release-notes-modal {:version "2.5"}]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:> app.main.ui.onboarding.team-choice/onboarding-team-modal*]
|
||||
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-team-modal?
|
||||
[:> onboarding-team-modal* {:go-to-team true}]
|
||||
show-team-modal?
|
||||
[:> onboarding-team-modal* {:go-to-team true}]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}])
|
||||
|
||||
[:> team-container* {:team-id team-id}
|
||||
[:> dashboard-page* {:profile profile
|
||||
:section section
|
||||
:team-id team-id
|
||||
:search-term search-term
|
||||
:plugin-url plugin-url
|
||||
:project-id project-id
|
||||
:template template
|
||||
:pending-action-id pending-action-id}]]])
|
||||
[:> team-container* {:team-id team-id}
|
||||
[:> dashboard-page* {:profile profile
|
||||
:section section
|
||||
:team-id team-id
|
||||
:search-term search-term
|
||||
:plugin-url plugin-url
|
||||
:project-id project-id
|
||||
:template template
|
||||
:pending-action-id pending-action-id}]]]))
|
||||
|
||||
:workspace
|
||||
(let [params (get params :query)
|
||||
team-id (some-> params :team-id uuid/parse*)
|
||||
file-id (some-> params :file-id uuid/parse*)
|
||||
page-id (some-> params :page-id uuid/parse*)
|
||||
layout (some-> params :layout keyword)]
|
||||
[:? {}
|
||||
(when (cf/external-feature-flag "onboarding-03" "test")
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
layout (some-> params :layout keyword)
|
||||
sso-error? (some? (:sso-error params))
|
||||
organization-id (some-> params :organization-id uuid/parse*)]
|
||||
(if sso-error?
|
||||
[:> static/sso-error* {:organization-id organization-id :profile profile :is-workspace true}]
|
||||
[:? {}
|
||||
(when (cf/external-feature-flag "onboarding-03" "test")
|
||||
(cond
|
||||
show-question-modal?
|
||||
[:& questions-modal]
|
||||
|
||||
show-team-modal?
|
||||
[:> onboarding-team-modal* {:go-to-team false}]
|
||||
show-team-modal?
|
||||
[:> onboarding-team-modal* {:go-to-team false}]
|
||||
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}]))
|
||||
show-release-modal?
|
||||
[:& release-notes-modal {:version (:main cf/version)}]))
|
||||
|
||||
[:> team-container* {:team-id team-id}
|
||||
[:> workspace-page* {:team-id team-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:layout-name layout
|
||||
:key file-id}]]])
|
||||
[:> team-container* {:team-id team-id}
|
||||
[:> workspace-page* {:team-id team-id
|
||||
:file-id file-id
|
||||
:page-id page-id
|
||||
:layout-name layout
|
||||
:key file-id}]]]))
|
||||
|
||||
:viewer
|
||||
(let [params (get params :query)
|
||||
|
||||
@ -108,7 +108,8 @@
|
||||
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?
|
||||
sso-error? (some? (get-in match [:query-params :sso-error]))
|
||||
team-id-str (when (and relevant? (not sso-error?))
|
||||
(or (get-in match [:query-params :team-id])
|
||||
(get-in match [:params :path :team-id])))
|
||||
team-id (some-> team-id-str uuid/parse*)]
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.auth :refer [is-authenticated?]]
|
||||
[app.main.data.common :as dcm]
|
||||
[app.main.data.nitrate :as dnt]
|
||||
[app.main.errors :as errors]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
@ -515,6 +516,46 @@
|
||||
|
||||
children])
|
||||
|
||||
(mf/defc sso-error*
|
||||
"Shown in place of the dashboard/workspace (same static skeleton and
|
||||
`request-dialog*` used by the no-permission dialogs) when the organization
|
||||
SSO exchange with the identity provider fails."
|
||||
[{:keys [organization-id profile is-workspace is-dashboard]}]
|
||||
(let [clean-url
|
||||
(mf/with-memo []
|
||||
(-> (rt/get-current-href)
|
||||
(dom/remove-query-param :sso-error)
|
||||
(dom/remove-query-param :organization-id)))
|
||||
|
||||
_ (mf/with-effect []
|
||||
;; Consume the marker once: scrub it from the URL bar so a
|
||||
;; browser refresh doesn't keep re-showing this dialog.
|
||||
(dom/replace-history-state! clean-url))
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(mf/deps profile)
|
||||
(fn []
|
||||
;; Land on the user's own default team
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id (:default-team-id profile)))))
|
||||
|
||||
on-retry
|
||||
(mf/use-fn
|
||||
(mf/deps organization-id clean-url)
|
||||
(fn []
|
||||
(st/emit! (dnt/retry-organization-sso {:organization-id organization-id
|
||||
:dest-url clean-url}))))]
|
||||
|
||||
[:> context-wrapper* {:is-dashboard (or is-dashboard (not is-workspace))
|
||||
:is-workspace is-workspace
|
||||
:profile profile}
|
||||
[:> request-dialog* {:title (tr "labels.sso-error.title")
|
||||
:content [(tr "labels.sso-error.desc-message")]
|
||||
:button-text (tr "labels.sso-error.retry")
|
||||
:on-button-click on-retry
|
||||
:cancel-text (tr "not-found.no-permission.go-dashboard")
|
||||
:on-close on-close}]]))
|
||||
|
||||
(mf/defc exception-page*
|
||||
[{:keys [data route] :as props}]
|
||||
|
||||
|
||||
@ -10339,3 +10339,12 @@ msgstr "Click to close the path"
|
||||
|
||||
msgid "notifications.invitation-canceled"
|
||||
msgstr "This invitation is no longer available."
|
||||
|
||||
msgid "labels.sso-error.title"
|
||||
msgstr "We couldn't sign you in to your organization"
|
||||
|
||||
msgid "labels.sso-error.desc-message"
|
||||
msgstr "Sign-in with your organization's identity provider didn't complete. The provider may be unavailable, or your account may not be in its directory yet. Your Penpot account isn't affected."
|
||||
|
||||
msgid "labels.sso-error.retry"
|
||||
msgstr "Try again"
|
||||
|
||||
@ -9989,3 +9989,13 @@ msgstr "Pulsar para cerrar la ruta"
|
||||
|
||||
msgid "notifications.invitation-canceled"
|
||||
msgstr "Esta invitación ya no está disponible."
|
||||
|
||||
msgid "labels.sso-error.title"
|
||||
msgstr "No pudimos iniciar sesión en tu organización"
|
||||
|
||||
msgid "labels.sso-error.desc-message"
|
||||
msgstr ""
|
||||
"El inicio de sesión con el proveedor de identidad de tu organización no se completó. Es posible que el proveedor no esté disponible o que tu cuenta aún no esté en su directorio. Tu cuenta de Penpot no se ha visto afectada."
|
||||
|
||||
msgid "labels.sso-error.retry"
|
||||
msgstr "Intentar de nuevo"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user