mirror of
https://github.com/penpot/penpot.git
synced 2026-06-20 14:22:08 +00:00
✨ Add nitrate sso auth wrapper to the api endpoints
This commit is contained in:
parent
42760c9703
commit
5d65a52a53
@ -456,7 +456,7 @@
|
||||
:grant-type (:grant_type params)
|
||||
:redirect-uri (:redirect_uri params))
|
||||
|
||||
(let [{:keys [status body]} (http/req cfg req)]
|
||||
(let [{:keys [status body]} (http/req cfg req {:skip-ssrf-check? (:skip-ssrf-check? provider)})]
|
||||
(if (= status 200)
|
||||
(let [data (json/decode body)
|
||||
data {:token/access (get data :access_token)
|
||||
@ -511,7 +511,7 @@
|
||||
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
|
||||
:timeout 6000
|
||||
:method :get}
|
||||
response (http/req cfg params)]
|
||||
response (http/req cfg params {:skip-ssrf-check? (:skip-ssrf-check? provider)})]
|
||||
|
||||
(l/trc :hint "user info response"
|
||||
:status (:status response)
|
||||
@ -887,6 +887,7 @@
|
||||
::http/client
|
||||
::setup/props
|
||||
::db/pool
|
||||
[:app.nitrate/client {:optional true} [:maybe :map]]
|
||||
[::providers schema:providers]])
|
||||
|
||||
(defmethod ig/assert-key ::routes
|
||||
|
||||
@ -226,6 +226,20 @@
|
||||
(-> (db/exec-one! cfg [sql (:profile-id session) (:id session)])
|
||||
(db/get-update-count))))
|
||||
|
||||
(def ^:private sql:clear-org-sso-sessions
|
||||
(str "UPDATE http_session_v2 "
|
||||
"SET props = props #- ARRAY['~:sso', ?]::text[] "
|
||||
"WHERE props IS NOT NULL "
|
||||
"AND jsonb_exists(props -> '~:sso', ?)"))
|
||||
|
||||
(defn clear-org-sso-sessions!
|
||||
"Remove the SSO entry for organization-id from the props of every
|
||||
session that currently holds it. The key is transit-encoded as the
|
||||
string '~u<uuid>' under the '~:sso' path."
|
||||
[pool organization-id]
|
||||
(let [org-key (str "~u" organization-id)]
|
||||
(db/exec! pool [sql:clear-org-sso-sessions org-key org-key])))
|
||||
|
||||
(defn- renew-session?
|
||||
[{:keys [id modified-at] :as session}]
|
||||
(or (string? id)
|
||||
|
||||
@ -265,7 +265,8 @@
|
||||
::oidc/providers (ig/ref ::oidc/providers)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
::email/whitelist (ig/ref ::email/whitelist)
|
||||
:app.nitrate/client (ig/ref :app.nitrate/client)}
|
||||
|
||||
::mgmt/routes
|
||||
{::db/pool (ig/ref ::db/pool)
|
||||
@ -325,7 +326,7 @@
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::rds/pool (ig/ref ::rds/pool)
|
||||
:app.nitrate/client (ig/ref :app.nitrate/client)
|
||||
:app.nitrate/client (ig/ref :app.nitrate/client)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::ldap/provider (ig/ref ::ldap/provider)
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
[app.http.session :as session]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.util.cache :as cache]
|
||||
[clojure.core :as c]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
@ -510,12 +509,12 @@
|
||||
;; UTILS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defonce ^:private sso-auth-cache
|
||||
(cache/create :expire "1h" :max-size 1024))
|
||||
|
||||
(defn- compute-sso-authorization
|
||||
[cfg team-id session]
|
||||
(let [sso (call cfg :get-org-sso-by-team {:team-id team-id})]
|
||||
(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
|
||||
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})]
|
||||
(if-not (:active sso)
|
||||
{:authorized true :sso sso}
|
||||
(if (or (:issuer sso) (:base-url sso))
|
||||
@ -529,23 +528,6 @@
|
||||
{:authorized authorized :sso sso})
|
||||
{:authorized false :sso sso}))))
|
||||
|
||||
(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
|
||||
with :authorized and :sso keys. Positive results are cached for 1h
|
||||
by [team-id session-id]; negative results are never cached so that
|
||||
a completed SSO login is reflected immediately."
|
||||
[cfg team-id request]
|
||||
(let [session (session/get-session request)
|
||||
session-id (:id session)]
|
||||
(if (some? session-id)
|
||||
(or (cache/get sso-auth-cache [team-id session-id])
|
||||
(let [result (compute-sso-authorization cfg team-id session)]
|
||||
(when (:authorized result)
|
||||
(cache/get sso-auth-cache [team-id session-id] (constantly result)))
|
||||
result))
|
||||
(compute-sso-authorization cfg team-id session))))
|
||||
|
||||
(defn add-nitrate-licence-to-profile
|
||||
"Enriches a profile map with subscription information from Nitrate.
|
||||
Adds a :subscription field containing the user's license details.
|
||||
|
||||
@ -27,8 +27,10 @@
|
||||
[app.main :as-alias main]
|
||||
[app.metrics :as mtx]
|
||||
[app.msgbus :as-alias mbus]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.redis :as rds]
|
||||
[app.rpc.climit :as climit]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.cond :as cond]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
@ -36,6 +38,7 @@
|
||||
[app.rpc.rlimit :as rlimit]
|
||||
[app.setup :as-alias setup]
|
||||
[app.storage :as-alias sto]
|
||||
[app.util.cache :as cache]
|
||||
[app.util.inet :as inet]
|
||||
[app.util.services :as sv]
|
||||
[clojure.spec.alpha :as s]
|
||||
@ -208,6 +211,78 @@
|
||||
::sm/explain (explain params)))))))
|
||||
f))
|
||||
|
||||
|
||||
(defonce ^:private org-sso-auth-cache
|
||||
(cache/create :expire "15m" :max-size 1024))
|
||||
|
||||
(defn invalidate-org-sso-cache-by-org!
|
||||
"Invalidates all org-SSO authorization cache entries for the given organization-id."
|
||||
[organization-id]
|
||||
(cache/invalidate-if! org-sso-auth-cache #(= (:organization-id %) organization-id)))
|
||||
|
||||
(defn- coerce-uuid [v]
|
||||
(cond (uuid? v) v
|
||||
(string? v) (uuid/parse* v)))
|
||||
|
||||
(defn- wrap-org-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)
|
||||
|
||||
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.
|
||||
|
||||
Only activates when:
|
||||
- Nitrate flag is enabled
|
||||
- Endpoint requires authentication (::auth true by default)
|
||||
- Endpoint is not marked with ::nitrate/org-sso false
|
||||
|
||||
Raises :org-sso-required error if user is not authorized in the org."
|
||||
[_ f mdata]
|
||||
(if (and (contains? cf/flags :nitrate)
|
||||
(::auth mdata true) ;; only for endpoints that needs auth
|
||||
(::nitrate/org-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 (coerce-uuid (:id params))
|
||||
team-id (or (coerce-uuid (:team-id params))
|
||||
(when (= id-type :team) id))
|
||||
project-id (or (coerce-uuid (:project-id params))
|
||||
(when (= id-type :project) id))
|
||||
file-id (or (coerce-uuid (: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)
|
||||
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)))
|
||||
request (-> (meta params) (get ::http/request))
|
||||
{:keys [authorized sso]} (nitrate/sso-session-authorized? cfg team-id request)
|
||||
entry {:authorized authorized
|
||||
:organization-id (:organization-id sso)}]
|
||||
(when authorized
|
||||
(cache/get org-sso-auth-cache cache-key (constantly entry)))
|
||||
entry))]
|
||||
(if (:authorized result)
|
||||
(f cfg params)
|
||||
(ex/raise :type :authentication
|
||||
:code :org-sso-required
|
||||
:hint "organization SSO authentication required")))
|
||||
(f cfg params))))
|
||||
f))
|
||||
|
||||
(defn- wrap
|
||||
[cfg f mdata]
|
||||
(as-> f $
|
||||
@ -220,7 +295,8 @@
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
(wrap-authentication cfg $ mdata)
|
||||
(wrap-org-sso cfg $ mdata)))
|
||||
|
||||
(defn- wrap-management
|
||||
[cfg f mdata]
|
||||
@ -232,7 +308,10 @@
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-params-validation cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
(wrap-authentication cfg $ mdata)
|
||||
(wrap-org-sso cfg $ mdata)))
|
||||
|
||||
|
||||
|
||||
(defn- process-method
|
||||
[cfg wrap-fn [f mdata]]
|
||||
|
||||
@ -165,6 +165,7 @@
|
||||
(sv/defmethod ::get-file
|
||||
"Retrieve a file by its ID. Only authenticated users."
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :file
|
||||
::cond/get-object #(get-minimal-file-with-perms %1 %2)
|
||||
::cond/key-fn get-file-etag
|
||||
::sm/params schema:get-file
|
||||
@ -601,6 +602,7 @@
|
||||
(sv/defmethod ::get-file-summary
|
||||
"Retrieve a file summary by its ID. Only authenticated users."
|
||||
{::doc/added "1.20"
|
||||
::rpc/id-type :file
|
||||
::sm/params schema:get-file-summary}
|
||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||
(check-read-permissions! cfg profile-id id)
|
||||
@ -669,6 +671,7 @@
|
||||
outbound library reference counts. Cheap alternative to `get-file`
|
||||
when only metrics are needed."
|
||||
{::doc/added "2.17"
|
||||
::rpc/id-type :file
|
||||
::sm/params schema:get-file-stats
|
||||
::sm/result schema:get-file-stats-result
|
||||
::db/transaction true}
|
||||
@ -842,6 +845,7 @@
|
||||
|
||||
(sv/defmethod ::rename-file
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :file
|
||||
::webhooks/event? true
|
||||
|
||||
::sm/webhook
|
||||
@ -1001,6 +1005,7 @@
|
||||
|
||||
(sv/defmethod ::set-file-shared
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :file
|
||||
::webhooks/event? true
|
||||
::sm/params schema:set-file-shared}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
@ -1056,6 +1061,7 @@
|
||||
|
||||
(sv/defmethod ::delete-file
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :file
|
||||
::webhooks/event? true
|
||||
::sm/params schema:delete-file}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
|
||||
@ -130,7 +130,8 @@
|
||||
;; database.
|
||||
|
||||
(sv/defmethod ::update-file
|
||||
{::climit/id [[:update-file/by-profile ::rpc/profile-id]
|
||||
{::rpc/id-type :file
|
||||
::climit/id [[:update-file/by-profile ::rpc/profile-id]
|
||||
[:update-file/global]]
|
||||
|
||||
::webhooks/event? true
|
||||
|
||||
@ -633,7 +633,8 @@
|
||||
re-authentication transparently if the user already has an active SSO session."
|
||||
{::rpc/auth true
|
||||
::doc/added "2.19"
|
||||
::sm/params schema:auth-sso}
|
||||
::sm/params schema:auth-sso
|
||||
::nitrate/org-sso false}
|
||||
[cfg {:keys [team-id url] :as params}]
|
||||
(let [request (rph/get-request params)
|
||||
{:keys [authorized sso]} (nitrate/sso-session-authorized? cfg team-id request)]
|
||||
|
||||
@ -157,6 +157,7 @@
|
||||
|
||||
(sv/defmethod ::get-project
|
||||
{::doc/added "1.18"
|
||||
::rpc/id-type :project
|
||||
::sm/params schema:get-project}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
@ -223,6 +224,7 @@
|
||||
|
||||
(sv/defmethod ::update-project-pin
|
||||
{::doc/added "1.18"
|
||||
::rpc/id-type :project
|
||||
::sm/params schema:update-project-pin
|
||||
::webhooks/batch-timeout (ct/duration "5s")
|
||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||
@ -244,6 +246,7 @@
|
||||
|
||||
(sv/defmethod ::rename-project
|
||||
{::doc/added "1.18"
|
||||
::rpc/id-type :project
|
||||
::sm/params schema:rename-project
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
@ -286,6 +289,7 @@
|
||||
|
||||
(sv/defmethod ::delete-project
|
||||
{::doc/added "1.18"
|
||||
::rpc/id-type :project
|
||||
::sm/params schema:delete-project
|
||||
::webhooks/event? true
|
||||
::db/transaction true}
|
||||
|
||||
@ -236,6 +236,7 @@
|
||||
|
||||
(sv/defmethod ::get-team
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :team
|
||||
::sm/params schema:get-team}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id file-id]}]
|
||||
(get-team pool :profile-id profile-id :team-id id :file-id file-id))
|
||||
@ -691,6 +692,7 @@
|
||||
|
||||
(sv/defmethod ::update-team
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :team
|
||||
::sm/params schema:update-team
|
||||
::db/transaction true}
|
||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id name]}]
|
||||
@ -766,6 +768,7 @@
|
||||
|
||||
(sv/defmethod ::leave-team
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :team
|
||||
::sm/params schema:leave-team
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
@ -828,6 +831,7 @@
|
||||
|
||||
(sv/defmethod ::delete-team
|
||||
{::doc/added "1.17"
|
||||
::rpc/id-type :team
|
||||
::sm/params schema:delete-team
|
||||
::db/transaction true}
|
||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||
|
||||
@ -18,10 +18,11 @@
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.email :as eml]
|
||||
[app.http.session :as session]
|
||||
[app.loggers.audit :as audit]
|
||||
[app.media :as media]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc :as rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.nitrate :as cnit]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
@ -823,3 +824,19 @@ LEFT JOIN profile AS p
|
||||
(run! (partial submit-nitrate-audit-event cfg) events))
|
||||
nil))
|
||||
|
||||
|
||||
;; ---- API: notify-org-sso-change
|
||||
|
||||
(sv/defmethod ::notify-org-sso-change
|
||||
"Nitrate notifies that an organization sso values have changed"
|
||||
{::doc/added "2.19"
|
||||
::sm/params [:map
|
||||
[:organization-id ::sm/uuid]
|
||||
[:updated-props ::sm/boolean]]
|
||||
::rpc/auth false}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [organization-id updated-props]}]
|
||||
(when updated-props
|
||||
(rpc/invalidate-org-sso-cache-by-org! organization-id)
|
||||
(session/clear-org-sso-sessions! pool organization-id))
|
||||
(notifications/notify-organization-change-sso cfg organization-id)
|
||||
nil)
|
||||
|
||||
@ -43,3 +43,11 @@
|
||||
:organization-name organization-name
|
||||
:teams teams
|
||||
:deleted-teams deleted-teams})))
|
||||
|
||||
(defn notify-organization-change-sso
|
||||
[cfg organization-id]
|
||||
(let [msgbus (::mbus/msgbus cfg)]
|
||||
(mbus/pub! msgbus
|
||||
:topic uuid/zero
|
||||
:message {:type :organization-change-sso
|
||||
:organization-id organization-id})))
|
||||
|
||||
@ -24,7 +24,8 @@
|
||||
|
||||
(defprotocol ICache
|
||||
(get [_ k] [_ k load-fn] "get cache entry")
|
||||
(invalidate! [_] [_ k] "invalidate cache"))
|
||||
(invalidate! [_] [_ k] "invalidate cache")
|
||||
(invalidate-if! [_ pred] "invalidate all entries whose value satisfies pred"))
|
||||
|
||||
(defprotocol ICacheStats
|
||||
(stats [_] "get stats"))
|
||||
@ -72,6 +73,10 @@
|
||||
(.invalidateAll ^Cache cache))
|
||||
(invalidate! [_ k]
|
||||
(.invalidate ^Cache cache ^Object k))
|
||||
(invalidate-if! [_ pred]
|
||||
(doseq [[k v] (.asMap ^Cache cache)]
|
||||
(when (pred v)
|
||||
(.invalidate ^Cache cache ^Object k))))
|
||||
|
||||
ICacheStats
|
||||
(stats [_]
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
[app.main.data.team :as dtm]
|
||||
[app.main.data.websocket :as dws]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.sse :as sse]
|
||||
@ -756,15 +757,35 @@
|
||||
(when fetch? ;; If the user belonged to the org
|
||||
(rx/of (dtm/fetch-teams)))))))))
|
||||
|
||||
(defn- handle-organization-change-sso
|
||||
[{:keys [organization-id]}]
|
||||
(ptk/reify ::handle-organization-change-sso
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [team-id (:current-team-id state)
|
||||
team (dm/get-in state [:teams team-id])
|
||||
org-id (dm/get-in team [:organization :id])]
|
||||
(when (= organization-id org-id)
|
||||
(let [url (rt/get-current-href)]
|
||||
(->> (rp/cmd! :auth-sso {:team-id team-id :url url})
|
||||
(rx/mapcat (fn [{:keys [authorized redirect-uri]}]
|
||||
(if authorized
|
||||
(rx/empty)
|
||||
(if redirect-uri
|
||||
(rx/of (rt/nav-raw :uri (str redirect-uri)))
|
||||
(rx/empty)))))))))))))
|
||||
|
||||
(defn- process-message
|
||||
[{:keys [type] :as msg}]
|
||||
(case type
|
||||
:notification (dcm/handle-notification msg)
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
:team-membership-change (dcm/team-membership-change msg)
|
||||
:team-org-change (handle-change-team-org msg)
|
||||
:user-org-change (handle-user-org-change msg)
|
||||
:organization-deleted (handle-organization-deleted msg)
|
||||
:notification (dcm/handle-notification msg)
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
:team-membership-change (dcm/team-membership-change msg)
|
||||
:team-org-change (handle-change-team-org msg)
|
||||
:user-org-change (handle-user-org-change msg)
|
||||
:organization-deleted (handle-organization-deleted msg)
|
||||
:organization-change-sso (handle-organization-change-sso msg)
|
||||
nil))
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user