mirror of
https://github.com/penpot/penpot.git
synced 2026-09-12 15:09:38 +00:00
✨ Add nitrate sso wards to organization navigation
This commit is contained in:
parent
08721127a3
commit
b984e7bbe8
@ -24,6 +24,7 @@
|
|||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
|
[app.nitrate :as nitrate]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
@ -42,9 +43,9 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- discover-oidc-config
|
(defn- discover-oidc-config
|
||||||
[cfg {:keys [base-uri] :as provider}]
|
[cfg {:keys [base-uri skip-ssrf-check?] :as provider}]
|
||||||
(let [uri (u/join base-uri ".well-known/openid-configuration")
|
(let [uri (u/join base-uri ".well-known/openid-configuration")
|
||||||
rsp (http/req cfg {:method :get :uri (dm/str uri)})]
|
rsp (http/req cfg {:method :get :uri (dm/str uri)} {:skip-ssrf-check? skip-ssrf-check?})]
|
||||||
|
|
||||||
(if (= 200 (:status rsp))
|
(if (= 200 (:status rsp))
|
||||||
(let [data (-> rsp :body json/decode)
|
(let [data (-> rsp :body json/decode)
|
||||||
@ -105,8 +106,8 @@
|
|||||||
keys))
|
keys))
|
||||||
|
|
||||||
(defn- fetch-oidc-jwks
|
(defn- fetch-oidc-jwks
|
||||||
[cfg jwks-uri]
|
[cfg jwks-uri {:keys [skip-ssrf-check?]}]
|
||||||
(let [{:keys [status body]} (http/req cfg {:method :get :uri jwks-uri})]
|
(let [{:keys [status body]} (http/req cfg {:method :get :uri jwks-uri} {:skip-ssrf-check? skip-ssrf-check?})]
|
||||||
(if (= 200 status)
|
(if (= 200 status)
|
||||||
(-> body json/decode :keys process-oidc-jwks)
|
(-> body json/decode :keys process-oidc-jwks)
|
||||||
(ex/raise :type ::internal
|
(ex/raise :type ::internal
|
||||||
@ -118,7 +119,8 @@
|
|||||||
"Fetch and Add (if possible) JWK's to the OIDC provider"
|
"Fetch and Add (if possible) JWK's to the OIDC provider"
|
||||||
[cfg provider]
|
[cfg provider]
|
||||||
(try
|
(try
|
||||||
(if-let [jwks (some->> (:jwks-uri provider) (fetch-oidc-jwks cfg))]
|
(if-let [jwks (when-let [jwks-uri (:jwks-uri provider)]
|
||||||
|
(fetch-oidc-jwks cfg jwks-uri {:skip-ssrf-check? (:skip-ssrf-check? provider)}))]
|
||||||
(assoc provider :jwks jwks)
|
(assoc provider :jwks jwks)
|
||||||
provider)
|
provider)
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
@ -409,9 +411,9 @@
|
|||||||
(defn- build-redirect-uri
|
(defn- build-redirect-uri
|
||||||
[]
|
[]
|
||||||
(let [public (u/uri (cf/get :public-uri))]
|
(let [public (u/uri (cf/get :public-uri))]
|
||||||
(str (assoc public :path (str "/api/auth/oidc/callback")))))
|
(str (assoc public :path "/api/auth/oidc/callback"))))
|
||||||
|
|
||||||
(defn- build-auth-redirect-uri
|
(defn build-auth-redirect-uri
|
||||||
[provider token]
|
[provider token]
|
||||||
(let [params {:client_id (:client-id provider)
|
(let [params {:client_id (:client-id provider)
|
||||||
:redirect_uri (build-redirect-uri)
|
:redirect_uri (build-redirect-uri)
|
||||||
@ -454,7 +456,7 @@
|
|||||||
:grant-type (:grant_type params)
|
:grant-type (:grant_type params)
|
||||||
:redirect-uri (:redirect_uri 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)
|
(if (= status 200)
|
||||||
(let [data (json/decode body)
|
(let [data (json/decode body)
|
||||||
data {:token/access (get data :access_token)
|
data {:token/access (get data :access_token)
|
||||||
@ -509,7 +511,7 @@
|
|||||||
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
|
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}
|
||||||
:timeout 6000
|
:timeout 6000
|
||||||
:method :get}
|
: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"
|
(l/trc :hint "user info response"
|
||||||
:status (:status response)
|
:status (:status response)
|
||||||
@ -755,6 +757,25 @@
|
|||||||
(assoc profile :props props'))
|
(assoc profile :props props'))
|
||||||
profile)))
|
profile)))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; ORG SSO HELPERS
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn prepare-org-sso-provider
|
||||||
|
"Build an OIDC provider map dynamically from the Nitrate org SSO config.
|
||||||
|
Uses OIDC discovery via :base-url (or :issuer as fallback) when
|
||||||
|
token/auth/user URIs are absent."
|
||||||
|
[cfg {:keys [client-id client-secret base-url issuer scopes]}]
|
||||||
|
(prepare-oidc-provider cfg
|
||||||
|
{:type "oidc"
|
||||||
|
:client-id client-id
|
||||||
|
:client-secret client-secret
|
||||||
|
:base-uri (some-> (or base-url issuer)
|
||||||
|
(str/rtrim "/")
|
||||||
|
(str "/"))
|
||||||
|
:scopes (into default-oidc-scopes (or scopes #{}))
|
||||||
|
:skip-ssrf-check? true}))
|
||||||
|
|
||||||
(defn- auth-handler
|
(defn- auth-handler
|
||||||
[cfg {:keys [params] :as request}]
|
[cfg {:keys [params] :as request}]
|
||||||
(let [provider (resolve-provider cfg params)
|
(let [provider (resolve-provider cfg params)
|
||||||
@ -779,64 +800,81 @@
|
|||||||
(try
|
(try
|
||||||
(let [code (get params :code)
|
(let [code (get params :code)
|
||||||
state (get params :state)
|
state (get params :state)
|
||||||
state (tokens/verify cfg {:token state :iss "oidc"})
|
state (tokens/verify cfg {:token state :iss "oidc"})]
|
||||||
|
|
||||||
provider (resolve-provider cfg state)
|
;; Org SSO flow: state carries :dest-url — exchange the authorization
|
||||||
info (get-info cfg provider state code)
|
;; code with the OIDC provider to verify authentication actually occurred.
|
||||||
profile (get-profile cfg (:email info))]
|
(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})
|
||||||
|
provider (prepare-org-sso-provider cfg sso)
|
||||||
|
;; verify token or throw error
|
||||||
|
_info (get-info cfg provider state code)
|
||||||
|
session (session/get-session request)
|
||||||
|
exp (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))
|
||||||
|
|
||||||
(cond
|
(let [provider (resolve-provider cfg state)
|
||||||
(not profile)
|
info (get-info cfg provider state code)
|
||||||
(cond
|
profile (get-profile cfg (:email info))]
|
||||||
(and (email.blacklist/enabled? cfg)
|
|
||||||
(email.blacklist/contains? cfg (:email info)))
|
|
||||||
(redirect-with-error "email-domain-not-allowed")
|
|
||||||
|
|
||||||
(and (email.whitelist/enabled? cfg)
|
(cond
|
||||||
(not (email.whitelist/contains? cfg (:email info))))
|
(not profile)
|
||||||
(redirect-with-error "email-domain-not-allowed")
|
(cond
|
||||||
|
(and (email.blacklist/enabled? cfg)
|
||||||
|
(email.blacklist/contains? cfg (:email info)))
|
||||||
|
(redirect-with-error "email-domain-not-allowed")
|
||||||
|
|
||||||
:else
|
(and (email.whitelist/enabled? cfg)
|
||||||
(if (or (contains? cf/flags :registration)
|
(not (email.whitelist/contains? cfg (:email info))))
|
||||||
(contains? cf/flags :oidc-registration))
|
(redirect-with-error "email-domain-not-allowed")
|
||||||
(redirect-to-register cfg info provider)
|
|
||||||
(redirect-with-error "registration-disabled")))
|
|
||||||
|
|
||||||
(:is-blocked profile)
|
:else
|
||||||
(redirect-with-error "profile-blocked")
|
(if (or (contains? cf/flags :registration)
|
||||||
|
(contains? cf/flags :oidc-registration))
|
||||||
|
(redirect-to-register cfg info provider)
|
||||||
|
(redirect-with-error "registration-disabled")))
|
||||||
|
|
||||||
(not (or (= (:auth-backend profile) (:type provider))
|
(:is-blocked profile)
|
||||||
(profile-has-provider-props? provider profile)
|
(redirect-with-error "profile-blocked")
|
||||||
(provider-has-email-verified? provider info)))
|
|
||||||
(redirect-with-error "auth-provider-not-allowed")
|
|
||||||
|
|
||||||
(not (:is-active profile))
|
(not (or (= (:auth-backend profile) (:type provider))
|
||||||
(let [info (assoc info :profile-id (:id profile))]
|
(profile-has-provider-props? provider profile)
|
||||||
(redirect-to-register cfg info provider))
|
(provider-has-email-verified? provider info)))
|
||||||
|
(redirect-with-error "auth-provider-not-allowed")
|
||||||
|
|
||||||
:else
|
(not (:is-active profile))
|
||||||
(let [sxf (session/create-fn cfg profile info)
|
(let [info (assoc info :profile-id (:id profile))]
|
||||||
token (or (:invitation-token info)
|
(redirect-to-register cfg info provider))
|
||||||
(tokens/generate cfg
|
|
||||||
{:iss :auth
|
|
||||||
:exp (ct/in-future "15m")
|
|
||||||
:profile-id (:id profile)}))
|
|
||||||
|
|
||||||
;; If proceed, update profile on the database
|
:else
|
||||||
profile (update-profile-with-info cfg profile info)
|
(let [sxf (session/create-fn cfg profile info)
|
||||||
|
token (or (:invitation-token info)
|
||||||
|
(tokens/generate cfg
|
||||||
|
{:iss :auth
|
||||||
|
:exp (ct/in-future "15m")
|
||||||
|
:profile-id (:id profile)}))
|
||||||
|
|
||||||
props (audit/profile->props profile)
|
;; If proceed, update profile on the database
|
||||||
context (d/without-nils {:external-session-id (:external-session-id info)})]
|
profile (update-profile-with-info cfg profile info)
|
||||||
|
|
||||||
(audit/submit cfg {:type "action"
|
props (audit/profile->props profile)
|
||||||
:name "login-with-oidc"
|
context (d/without-nils {:external-session-id (:external-session-id info)})]
|
||||||
:profile-id (:id profile)
|
|
||||||
:ip-addr (inet/parse-request request)
|
|
||||||
:props props
|
|
||||||
:context context})
|
|
||||||
|
|
||||||
(->> (redirect-to-verify-token token)
|
(audit/submit cfg {:type "action"
|
||||||
(sxf request)))))
|
:name "login-with-oidc"
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:ip-addr (inet/parse-request request)
|
||||||
|
:props props
|
||||||
|
:context context})
|
||||||
|
|
||||||
|
(->> (redirect-to-verify-token token)
|
||||||
|
(sxf request)))))))
|
||||||
|
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(binding [l/*context* (errors/request->context request)]
|
(binding [l/*context* (errors/request->context request)]
|
||||||
@ -849,6 +887,7 @@
|
|||||||
::http/client
|
::http/client
|
||||||
::setup/props
|
::setup/props
|
||||||
::db/pool
|
::db/pool
|
||||||
|
[:app.nitrate/client [:maybe :map]]
|
||||||
[::providers schema:providers]])
|
[::providers schema:providers]])
|
||||||
|
|
||||||
(defmethod ig/assert-key ::routes
|
(defmethod ig/assert-key ::routes
|
||||||
|
|||||||
@ -68,17 +68,24 @@
|
|||||||
(def ^:private valid-params?
|
(def ^:private valid-params?
|
||||||
(sm/validator schema:params))
|
(sm/validator schema:params))
|
||||||
|
|
||||||
|
(defn- decode-session
|
||||||
|
[session]
|
||||||
|
(cond-> session
|
||||||
|
(db/pgobject? (:props session))
|
||||||
|
(update :props db/decode-transit-pgobject)))
|
||||||
|
|
||||||
(defn- database-manager
|
(defn- database-manager
|
||||||
[pool]
|
[pool]
|
||||||
(reify ISessionManager
|
(reify ISessionManager
|
||||||
(read-session [_ id]
|
(read-session [_ id]
|
||||||
(if (string? id)
|
(if (string? id)
|
||||||
;; Backward compatibility
|
;; Backward compatibility: http_session (v1) has no props column
|
||||||
(let [session (db/exec-one! pool (sql/select :http-session {:id id}))]
|
(let [session (db/exec-one! pool (sql/select :http-session {:id id}))]
|
||||||
(-> session
|
(-> session
|
||||||
(assoc :modified-at (:updated-at session))
|
(assoc :modified-at (:updated-at session))
|
||||||
(dissoc :updated-at)))
|
(dissoc :updated-at)))
|
||||||
(db/exec-one! pool (sql/select :http-session-v2 {:id id}))))
|
(some-> (db/exec-one! pool (sql/select :http-session-v2 {:id id}))
|
||||||
|
(decode-session))))
|
||||||
|
|
||||||
(create-session [_ params]
|
(create-session [_ params]
|
||||||
(assert (valid-params? params) "expect valid session params")
|
(assert (valid-params? params) "expect valid session params")
|
||||||
@ -100,7 +107,9 @@
|
|||||||
(assoc :created-at modified-at)
|
(assoc :created-at modified-at)
|
||||||
(assoc :modified-at modified-at)))
|
(assoc :modified-at modified-at)))
|
||||||
(db/update! pool :http-session-v2
|
(db/update! pool :http-session-v2
|
||||||
{:modified-at modified-at}
|
(cond-> {:modified-at modified-at}
|
||||||
|
(some? (:props session))
|
||||||
|
(assoc :props (db/tjson (:props session))))
|
||||||
{:id (:id session)}
|
{:id (:id session)}
|
||||||
{::db/return-keys true}))))
|
{::db/return-keys true}))))
|
||||||
|
|
||||||
@ -129,9 +138,10 @@
|
|||||||
session))
|
session))
|
||||||
|
|
||||||
(update-session [_ session]
|
(update-session [_ session]
|
||||||
(let [modified-at (ct/now)]
|
(let [modified-at (ct/now)
|
||||||
(swap! cache update (:id session) assoc :modified-at modified-at)
|
session (assoc session :modified-at modified-at)]
|
||||||
(assoc session :modified-at modified-at)))
|
(swap! cache assoc (:id session) session)
|
||||||
|
session))
|
||||||
|
|
||||||
(delete-session [_ id]
|
(delete-session [_ id]
|
||||||
(swap! cache dissoc id)
|
(swap! cache dissoc id)
|
||||||
@ -216,6 +226,20 @@
|
|||||||
(-> (db/exec-one! cfg [sql (:profile-id session) (:id session)])
|
(-> (db/exec-one! cfg [sql (:profile-id session) (:id session)])
|
||||||
(db/get-update-count))))
|
(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?
|
(defn- renew-session?
|
||||||
[{:keys [id modified-at] :as session}]
|
[{:keys [id modified-at] :as session}]
|
||||||
(or (string? id)
|
(or (string? id)
|
||||||
|
|||||||
@ -261,7 +261,8 @@
|
|||||||
::oidc/providers (ig/ref ::oidc/providers)
|
::oidc/providers (ig/ref ::oidc/providers)
|
||||||
::session/manager (ig/ref ::session/manager)
|
::session/manager (ig/ref ::session/manager)
|
||||||
::email/blacklist (ig/ref ::email/blacklist)
|
::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
|
::mgmt/routes
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
|
|||||||
@ -484,7 +484,10 @@
|
|||||||
:fn (mg/resource "app/migrations/sql/0148-add-variant-name-team-font-variant.sql")}
|
:fn (mg/resource "app/migrations/sql/0148-add-variant-name-team-font-variant.sql")}
|
||||||
|
|
||||||
{:name "0149-mod-file-library-rel-synced-at"
|
{:name "0149-mod-file-library-rel-synced-at"
|
||||||
:fn (mg/resource "app/migrations/sql/0149-mod-file-library-rel-synced-at.sql")}])
|
:fn (mg/resource "app/migrations/sql/0149-mod-file-library-rel-synced-at.sql")}
|
||||||
|
|
||||||
|
{:name "0150-mod-http-session-v2"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0150-mod-http-session-v2.sql")}])
|
||||||
|
|
||||||
(defn apply-migrations!
|
(defn apply-migrations!
|
||||||
[pool name migrations]
|
[pool name migrations]
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE http_session_v2
|
||||||
|
ADD COLUMN props jsonb NULL;
|
||||||
@ -17,6 +17,7 @@
|
|||||||
[app.common.types.organization :as cto]
|
[app.common.types.organization :as cto]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.http.client :as http]
|
[app.http.client :as http]
|
||||||
|
[app.http.session :as session]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
@ -425,6 +426,28 @@
|
|||||||
[:permissions [:map-of :keyword :string]]]
|
[:permissions [:map-of :keyword :string]]]
|
||||||
params)))
|
params)))
|
||||||
|
|
||||||
|
(def ^:private schema:nitrate-sso
|
||||||
|
[:map
|
||||||
|
[:organization-id ::sm/uuid]
|
||||||
|
[:active [:maybe :boolean]]
|
||||||
|
[:provider [:maybe :string]]
|
||||||
|
[:client-id [:maybe :string]]
|
||||||
|
[:base-url [:maybe :string]]
|
||||||
|
[:client-secret [:maybe :string]]
|
||||||
|
[:issuer [:maybe :string]]
|
||||||
|
[:scopes [:maybe [::sm/set ::sm/text]]]])
|
||||||
|
|
||||||
|
(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)))
|
||||||
|
|
||||||
(defn- get-org-members-api
|
(defn- get-org-members-api
|
||||||
[cfg {:keys [organization-id] :as params}]
|
[cfg {:keys [organization-id] :as params}]
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||||
@ -463,6 +486,7 @@
|
|||||||
:add-profile-to-org (partial add-profile-to-org-api cfg)
|
:add-profile-to-org (partial add-profile-to-org-api cfg)
|
||||||
:remove-profile-from-org (partial remove-profile-from-org-api cfg)
|
:remove-profile-from-org (partial remove-profile-from-org-api cfg)
|
||||||
:get-org-permissions (partial get-org-permissions-api cfg)
|
:get-org-permissions (partial get-org-permissions-api cfg)
|
||||||
|
:get-org-sso-by-team (partial get-org-sso-by-team-api cfg)
|
||||||
:delete-team (partial delete-team-api cfg)
|
:delete-team (partial delete-team-api cfg)
|
||||||
:remove-team-from-org (partial remove-team-from-org-api cfg)
|
:remove-team-from-org (partial remove-team-from-org-api cfg)
|
||||||
:get-subscription (partial get-subscription-api cfg)
|
:get-subscription (partial get-subscription-api cfg)
|
||||||
@ -474,6 +498,24 @@
|
|||||||
;; UTILS
|
;; UTILS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(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))
|
||||||
|
(let [props (:props session)
|
||||||
|
sso-map (get props :sso {})
|
||||||
|
organization-id (:organization-id sso)
|
||||||
|
exp (get sso-map organization-id)
|
||||||
|
now (ct/now)
|
||||||
|
authorized (and (ct/inst? exp)
|
||||||
|
(ct/is-after? exp now))]
|
||||||
|
{:authorized authorized :sso sso})
|
||||||
|
{:authorized false :sso sso}))))
|
||||||
|
|
||||||
(defn add-nitrate-licence-to-profile
|
(defn add-nitrate-licence-to-profile
|
||||||
"Enriches a profile map with subscription information from Nitrate.
|
"Enriches a profile map with subscription information from Nitrate.
|
||||||
|
|||||||
@ -27,8 +27,10 @@
|
|||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
[app.msgbus :as-alias mbus]
|
[app.msgbus :as-alias mbus]
|
||||||
|
[app.nitrate :as nitrate]
|
||||||
[app.redis :as rds]
|
[app.redis :as rds]
|
||||||
[app.rpc.climit :as climit]
|
[app.rpc.climit :as climit]
|
||||||
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.cond :as cond]
|
[app.rpc.cond :as cond]
|
||||||
[app.rpc.doc :as doc]
|
[app.rpc.doc :as doc]
|
||||||
[app.rpc.helpers :as rph]
|
[app.rpc.helpers :as rph]
|
||||||
@ -36,6 +38,7 @@
|
|||||||
[app.rpc.rlimit :as rlimit]
|
[app.rpc.rlimit :as rlimit]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[app.storage :as-alias sto]
|
[app.storage :as-alias sto]
|
||||||
|
[app.util.cache :as cache]
|
||||||
[app.util.inet :as inet]
|
[app.util.inet :as inet]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
@ -208,6 +211,74 @@
|
|||||||
::sm/explain (explain params)))))))
|
::sm/explain (explain params)))))))
|
||||||
f))
|
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- 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)
|
||||||
|
|
||||||
|
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 :nitrate-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/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)
|
||||||
|
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 :nitrate-sso-required
|
||||||
|
:hint "organization SSO authentication required")))
|
||||||
|
(f cfg params))))
|
||||||
|
f))
|
||||||
|
|
||||||
(defn- wrap
|
(defn- wrap
|
||||||
[cfg f mdata]
|
[cfg f mdata]
|
||||||
(as-> f $
|
(as-> f $
|
||||||
@ -220,7 +291,8 @@
|
|||||||
(wrap-audit cfg $ mdata)
|
(wrap-audit cfg $ mdata)
|
||||||
(wrap-spec-conform cfg $ mdata)
|
(wrap-spec-conform cfg $ mdata)
|
||||||
(wrap-params-validation cfg $ mdata)
|
(wrap-params-validation cfg $ mdata)
|
||||||
(wrap-authentication cfg $ mdata)))
|
(wrap-authentication cfg $ mdata)
|
||||||
|
(wrap-nitrate-sso cfg $ mdata)))
|
||||||
|
|
||||||
(defn- wrap-management
|
(defn- wrap-management
|
||||||
[cfg f mdata]
|
[cfg f mdata]
|
||||||
@ -232,7 +304,10 @@
|
|||||||
(wrap-audit cfg $ mdata)
|
(wrap-audit cfg $ mdata)
|
||||||
(wrap-spec-conform cfg $ mdata)
|
(wrap-spec-conform cfg $ mdata)
|
||||||
(wrap-params-validation cfg $ mdata)
|
(wrap-params-validation cfg $ mdata)
|
||||||
(wrap-authentication cfg $ mdata)))
|
(wrap-authentication cfg $ mdata)
|
||||||
|
(wrap-nitrate-sso cfg $ mdata)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(defn- process-method
|
(defn- process-method
|
||||||
[cfg wrap-fn [f mdata]]
|
[cfg wrap-fn [f mdata]]
|
||||||
|
|||||||
@ -165,6 +165,7 @@
|
|||||||
(sv/defmethod ::get-file
|
(sv/defmethod ::get-file
|
||||||
"Retrieve a file by its ID. Only authenticated users."
|
"Retrieve a file by its ID. Only authenticated users."
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :file
|
||||||
::cond/get-object #(get-minimal-file-with-perms %1 %2)
|
::cond/get-object #(get-minimal-file-with-perms %1 %2)
|
||||||
::cond/key-fn get-file-etag
|
::cond/key-fn get-file-etag
|
||||||
::sm/params schema:get-file
|
::sm/params schema:get-file
|
||||||
@ -601,6 +602,7 @@
|
|||||||
(sv/defmethod ::get-file-summary
|
(sv/defmethod ::get-file-summary
|
||||||
"Retrieve a file summary by its ID. Only authenticated users."
|
"Retrieve a file summary by its ID. Only authenticated users."
|
||||||
{::doc/added "1.20"
|
{::doc/added "1.20"
|
||||||
|
::rpc/id-type :file
|
||||||
::sm/params schema:get-file-summary}
|
::sm/params schema:get-file-summary}
|
||||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||||
(check-read-permissions! cfg profile-id id)
|
(check-read-permissions! cfg profile-id id)
|
||||||
@ -669,6 +671,7 @@
|
|||||||
outbound library reference counts. Cheap alternative to `get-file`
|
outbound library reference counts. Cheap alternative to `get-file`
|
||||||
when only metrics are needed."
|
when only metrics are needed."
|
||||||
{::doc/added "2.17"
|
{::doc/added "2.17"
|
||||||
|
::rpc/id-type :file
|
||||||
::sm/params schema:get-file-stats
|
::sm/params schema:get-file-stats
|
||||||
::sm/result schema:get-file-stats-result
|
::sm/result schema:get-file-stats-result
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
@ -842,6 +845,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::rename-file
|
(sv/defmethod ::rename-file
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :file
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
|
|
||||||
::sm/webhook
|
::sm/webhook
|
||||||
@ -1001,6 +1005,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::set-file-shared
|
(sv/defmethod ::set-file-shared
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :file
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:set-file-shared}
|
::sm/params schema:set-file-shared}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||||
@ -1056,6 +1061,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-file
|
(sv/defmethod ::delete-file
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :file
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:delete-file}
|
::sm/params schema:delete-file}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||||
|
|||||||
@ -130,7 +130,8 @@
|
|||||||
;; database.
|
;; database.
|
||||||
|
|
||||||
(sv/defmethod ::update-file
|
(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]]
|
[:update-file/global]]
|
||||||
|
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
"Nitrate API for Penpot. Provides nitrate-related endpoints to be called
|
"Nitrate API for Penpot. Provides nitrate-related endpoints to be called
|
||||||
from Penpot frontend."
|
from Penpot frontend."
|
||||||
(:require
|
(:require
|
||||||
|
[app.auth.oidc :as oidc]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
@ -19,7 +20,9 @@
|
|||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
|
[app.rpc.helpers :as rph]
|
||||||
[app.rpc.notifications :as notifications]
|
[app.rpc.notifications :as notifications]
|
||||||
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]))
|
[app.util.services :as sv]))
|
||||||
|
|
||||||
|
|
||||||
@ -617,3 +620,39 @@
|
|||||||
:allows-anybody false}))
|
:allows-anybody false}))
|
||||||
|
|
||||||
|
|
||||||
|
(def ^:private schema:check-nitrate-sso
|
||||||
|
[:map {:title "AuthSsoParams"}
|
||||||
|
[:team-id ::sm/uuid]
|
||||||
|
[:url ::sm/uri]])
|
||||||
|
|
||||||
|
(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.
|
||||||
|
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."
|
||||||
|
{::rpc/auth true
|
||||||
|
::doc/added "2.19"
|
||||||
|
::sm/params schema:check-nitrate-sso
|
||||||
|
::nitrate/sso false}
|
||||||
|
[cfg {:keys [team-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)]
|
||||||
|
(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)]
|
||||||
|
{:authorized false
|
||||||
|
:redirect-uri redirect-uri})
|
||||||
|
{:authorized false
|
||||||
|
:redirect-uri nil})))
|
||||||
|
{:authorized true}))
|
||||||
|
|||||||
@ -157,6 +157,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-project
|
(sv/defmethod ::get-project
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
|
::rpc/id-type :project
|
||||||
::sm/params schema:get-project}
|
::sm/params schema:get-project}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id]}]
|
||||||
(dm/with-open [conn (db/open pool)]
|
(dm/with-open [conn (db/open pool)]
|
||||||
@ -223,6 +224,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::update-project-pin
|
(sv/defmethod ::update-project-pin
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
|
::rpc/id-type :project
|
||||||
::sm/params schema:update-project-pin
|
::sm/params schema:update-project-pin
|
||||||
::webhooks/batch-timeout (ct/duration "5s")
|
::webhooks/batch-timeout (ct/duration "5s")
|
||||||
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id)
|
||||||
@ -244,6 +246,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::rename-project
|
(sv/defmethod ::rename-project
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
|
::rpc/id-type :project
|
||||||
::sm/params schema:rename-project
|
::sm/params schema:rename-project
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
@ -286,6 +289,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-project
|
(sv/defmethod ::delete-project
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
|
::rpc/id-type :project
|
||||||
::sm/params schema:delete-project
|
::sm/params schema:delete-project
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
|
|||||||
@ -236,6 +236,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::get-team
|
(sv/defmethod ::get-team
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :team
|
||||||
::sm/params schema:get-team}
|
::sm/params schema:get-team}
|
||||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id id file-id]}]
|
[{:keys [::db/pool]} {:keys [::rpc/profile-id id file-id]}]
|
||||||
(get-team pool :profile-id profile-id :team-id id :file-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
|
(sv/defmethod ::update-team
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :team
|
||||||
::sm/params schema:update-team
|
::sm/params schema:update-team
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id name]}]
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id name]}]
|
||||||
@ -766,6 +768,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::leave-team
|
(sv/defmethod ::leave-team
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :team
|
||||||
::sm/params schema:leave-team
|
::sm/params schema:leave-team
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||||
@ -828,6 +831,7 @@
|
|||||||
|
|
||||||
(sv/defmethod ::delete-team
|
(sv/defmethod ::delete-team
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
::rpc/id-type :team
|
||||||
::sm/params schema:delete-team
|
::sm/params schema:delete-team
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[cfg {:keys [::rpc/profile-id id] :as params}]
|
[cfg {:keys [::rpc/profile-id id] :as params}]
|
||||||
|
|||||||
@ -18,10 +18,11 @@
|
|||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.email :as eml]
|
[app.email :as eml]
|
||||||
|
[app.http.session :as session]
|
||||||
[app.loggers.audit :as audit]
|
[app.loggers.audit :as audit]
|
||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.nitrate :as nitrate]
|
[app.nitrate :as nitrate]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as rpc]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.nitrate :as cnit]
|
[app.rpc.commands.nitrate :as cnit]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
@ -823,3 +824,19 @@ LEFT JOIN profile AS p
|
|||||||
(run! (partial submit-nitrate-audit-event cfg) events))
|
(run! (partial submit-nitrate-audit-event cfg) events))
|
||||||
nil))
|
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
|
:organization-name organization-name
|
||||||
:teams teams
|
:teams teams
|
||||||
:deleted-teams deleted-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
|
(defprotocol ICache
|
||||||
(get [_ k] [_ k load-fn] "get cache entry")
|
(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
|
(defprotocol ICacheStats
|
||||||
(stats [_] "get stats"))
|
(stats [_] "get stats"))
|
||||||
@ -68,10 +69,14 @@
|
|||||||
^Function (reify Function
|
^Function (reify Function
|
||||||
(apply [_ k]
|
(apply [_ k]
|
||||||
(load-fn k)))))
|
(load-fn k)))))
|
||||||
(invalidate! [_]
|
(invalidate [_]
|
||||||
(.invalidateAll ^Cache cache))
|
(.invalidateAll ^Cache cache))
|
||||||
(invalidate! [_ k]
|
(invalidate [_ k]
|
||||||
(.invalidate ^Cache cache ^Object k))
|
(.invalidate ^Cache cache ^Object k))
|
||||||
|
(invalidate-if [_ pred]
|
||||||
|
(doseq [[k v] (.asMap ^Cache cache)]
|
||||||
|
(when (pred v)
|
||||||
|
(.invalidate ^Cache cache ^Object k))))
|
||||||
|
|
||||||
ICacheStats
|
ICacheStats
|
||||||
(stats [_]
|
(stats [_]
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
[:avatar-bg-url ::sm/uri]
|
[:avatar-bg-url ::sm/uri]
|
||||||
[:logo-id {:optional true} [:maybe ::sm/uuid]]
|
[:logo-id {:optional true} [:maybe ::sm/uuid]]
|
||||||
[:expired-license {:optional true} [:maybe :boolean]]
|
[:expired-license {:optional true} [:maybe :boolean]]
|
||||||
|
[:sso-active {:optional true} [:maybe :boolean]]
|
||||||
[:permissions {:optional true}
|
[:permissions {:optional true}
|
||||||
[:maybe [:map
|
[:maybe [:map
|
||||||
[:create-teams {:optional true} [:maybe [:enum "any" "onlyMe"]]]
|
[:create-teams {:optional true} [:maybe [:enum "any" "onlyMe"]]]
|
||||||
@ -33,7 +34,7 @@
|
|||||||
|
|
||||||
(def organization->team-keys
|
(def organization->team-keys
|
||||||
"Organization field keys to include in the nested :organization map."
|
"Organization field keys to include in the nested :organization map."
|
||||||
[:id :name :custom-photo :slug :avatar-bg-url :owner-id :expired-license :permissions])
|
[:id :name :custom-photo :slug :avatar-bg-url :owner-id :expired-license :permissions :sso-active])
|
||||||
|
|
||||||
(defn apply-organization
|
(defn apply-organization
|
||||||
"Updates a team map with organization fields in a nested :organization map.
|
"Updates a team map with organization fields in a nested :organization map.
|
||||||
|
|||||||
@ -183,3 +183,8 @@
|
|||||||
;; (js/console.log "RES: " res))
|
;; (js/console.log "RES: " res))
|
||||||
;;
|
;;
|
||||||
;; )))
|
;; )))
|
||||||
|
|
||||||
|
|
||||||
|
(defn coerce [v]
|
||||||
|
(cond (uuid? v) v
|
||||||
|
(string? v) (parse* v)))
|
||||||
@ -27,6 +27,7 @@
|
|||||||
[app.main.data.team :as dtm]
|
[app.main.data.team :as dtm]
|
||||||
[app.main.data.websocket :as dws]
|
[app.main.data.websocket :as dws]
|
||||||
[app.main.repo :as rp]
|
[app.main.repo :as rp]
|
||||||
|
[app.main.router :as rt]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.sse :as sse]
|
[app.util.sse :as sse]
|
||||||
@ -756,15 +757,35 @@
|
|||||||
(when fetch? ;; If the user belonged to the org
|
(when fetch? ;; If the user belonged to the org
|
||||||
(rx/of (dtm/fetch-teams)))))))))
|
(rx/of (dtm/fetch-teams)))))))))
|
||||||
|
|
||||||
|
(defn- handle-nitrate-change-sso
|
||||||
|
[{:keys [organization-id]}]
|
||||||
|
(ptk/reify ::handle-nitrate-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! :check-nitrate-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
|
(defn- process-message
|
||||||
[{:keys [type] :as msg}]
|
[{:keys [type] :as msg}]
|
||||||
(case type
|
(case type
|
||||||
:notification (dcm/handle-notification msg)
|
:notification (dcm/handle-notification msg)
|
||||||
:team-role-change (handle-change-team-role msg)
|
:team-role-change (handle-change-team-role msg)
|
||||||
:team-membership-change (dcm/team-membership-change msg)
|
:team-membership-change (dcm/team-membership-change msg)
|
||||||
:team-org-change (handle-change-team-org msg)
|
:team-org-change (handle-change-team-org msg)
|
||||||
:user-org-change (handle-user-org-change msg)
|
:user-org-change (handle-user-org-change msg)
|
||||||
:organization-deleted (handle-organization-deleted msg)
|
:organization-deleted (handle-organization-deleted msg)
|
||||||
|
:organization-change-sso (handle-nitrate-change-sso msg)
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -97,6 +97,31 @@
|
|||||||
(swap! storage/session assoc
|
(swap! storage/session assoc
|
||||||
:plugin-url plugin))))
|
:plugin-url plugin))))
|
||||||
|
|
||||||
|
(defn- check-sso-and-navigate
|
||||||
|
"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."
|
||||||
|
[match send-event-info? url]
|
||||||
|
(let [route-name (name (get-in match [:data :name]))
|
||||||
|
relevant? (and (contains? cf/flags :nitrate)
|
||||||
|
(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)
|
||||||
|
(->> (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))))))
|
||||||
|
(fn [cause]
|
||||||
|
(errors/on-error cause))))
|
||||||
|
(st/emit! (rt/navigated match send-event-info?)))))
|
||||||
|
|
||||||
(defn on-navigate
|
(defn on-navigate
|
||||||
[router path send-event-info?]
|
[router path send-event-info?]
|
||||||
(let [location (.-location js/document)
|
(let [location (.-location js/document)
|
||||||
@ -112,7 +137,7 @@
|
|||||||
(st/emit! (rt/assign-exception {:type :not-found}))
|
(st/emit! (rt/assign-exception {:type :not-found}))
|
||||||
|
|
||||||
(some? match)
|
(some? match)
|
||||||
(st/emit! (rt/navigated match send-event-info?))
|
(check-sso-and-navigate match send-event-info? (rt/get-current-href))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
;; We just recheck with an additional profile request; this
|
;; We just recheck with an additional profile request; this
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user