diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 25d419a354e..621df175582 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -106,6 +106,8 @@ services: environment: << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri] + # Uncomment for SSO: hides Account → Password (injected into config.js by the frontend image entrypoint). + # AUTH_TYPE: SSO penpot-backend: image: "penpotapp/backend:${PENPOT_VERSION:-2.15}" diff --git a/docker/images/files/config.js b/docker/images/files/config.js index a9f444caa64..967b3b8be25 100644 --- a/docker/images/files/config.js +++ b/docker/images/files/config.js @@ -1,3 +1,5 @@ // Frontend configuration //var penpotFlags = ""; //var penpotMpassSignoutUrl = ""; +// Deploy-time auth mode (injected via AUTH_TYPE env, e.g. SSO). Matches case-insensitively; "sso" hides password settings. +//var penpotAuthType = ""; diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index ace4bdfb544..3008eaac3c0 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -38,8 +38,39 @@ update_mpass_signout_url() { fi } +# AUTH_TYPE (e.g. SSO): writes penpotAuthType into frontend config.js. +# +# Normalize: fold to lowercase, then validate against a strict whitelist of +# safe characters ([a-z0-9_-]). Any value that contains special characters +# (spaces, #, &, \, ", newlines, …) is rejected and the line is left commented. +# The temp file is created in the same directory as the target so that the +# final 'mv' is always an atomic rename on the same filesystem (avoids EXDEV). +update_auth_type() { + if [ -n "${AUTH_TYPE:-}" ]; then + local auth_norm tmp + auth_norm=$(printf '%s' "$AUTH_TYPE" | LC_ALL=C tr '[:upper:]' '[:lower:]') + # Reject values that contain anything outside a-z 0-9 _ - + if ! [[ "$auth_norm" =~ ^[a-z0-9_-]+$ ]]; then + echo "update_auth_type: AUTH_TYPE contains invalid characters; skipping injection" >&2 + return 0 + fi + tmp="$(mktemp -p "$(dirname "$1")")" || return 1 + if ! sed \ + -e "s#^//var penpotAuthType = .*;#var penpotAuthType = \"${auth_norm}\";#g" \ + "$1" > "$tmp"; then + rm -f "$tmp" + return 1 + fi + if ! mv "$tmp" "$1"; then + rm -f "$tmp" + return 1 + fi + fi +} + update_flags /var/www/app/js/config.js update_mpass_signout_url /var/www/app/js/config.js +update_auth_type /var/www/app/js/config.js ######################################### ## Nginx Config diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index ae9f153d386..94f99aa319c 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -161,6 +161,16 @@ ;; penpot /auth/login screen so the oauth2-proxy cookie and Cognito ;; session are also cleared. Nil on non-SSO deployments. (def mpass-signout-url (obj/get global "penpotMpassSignoutUrl")) + +(defn ^boolean auth-type-sso? + "True when `penpotAuthType` is set in config.js from deploy env AUTH_TYPE, + normalized to SSO (case-insensitive). Enables hiding password/account flows + backed by Penpot-local credentials." + [] + (let [v (obj/get global "penpotAuthType")] + (and (string? v) + (= "sso" (-> v str/trim str/lower))))) + (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def plugins-list-uri (obj/get global "penpotPluginsListURI" "https://penpot.app/penpothub/plugins")) diff --git a/frontend/src/app/main/ui/settings.cljs b/frontend/src/app/main/ui/settings.cljs index 2cb617c9392..766150ff108 100644 --- a/frontend/src/app/main/ui/settings.cljs +++ b/frontend/src/app/main/ui/settings.cljs @@ -7,10 +7,12 @@ (ns app.main.ui.settings (:require-macros [app.main.style :as stl]) (:require + [app.config :as cf] [app.main.data.dashboard.shortcuts :as sc] [app.main.refs :as refs] [app.main.router :as rt] [app.main.store :as st] + [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.hooks :as hooks] [app.main.ui.modal :refer [modal-container*]] [app.main.ui.settings.change-email] @@ -40,9 +42,14 @@ (hooks/use-shortcuts ::dashboard sc/shortcuts) - (mf/with-effect [profile] + (mf/with-effect [section profile] (when (nil? profile) - (st/emit! (rt/assign-exception {:type :authentication})))) + (st/emit! (rt/assign-exception {:type :authentication}))) + ;; Wait for profile so we don't race assign-exception vs redirect on cold load. + (when (and (= section :settings-password) + (some? profile) + (cf/auth-type-sso?)) + (st/emit! (rt/nav :settings-profile {} {::rt/replace true})))) [:* [:> modal-container*] @@ -65,7 +72,12 @@ :error-href error-href}] :settings-password - [:& password-page] + (if (cf/auth-type-sso?) + (if (some? profile) + [:& profile-page] + [:> loader* + {:title (tr "labels.loading") :overlay false}]) + [:& password-page]) :settings-options [:& options-page] diff --git a/frontend/src/app/main/ui/settings/sidebar.cljs b/frontend/src/app/main/ui/settings/sidebar.cljs index 7f35f4787fa..66778daa1ac 100644 --- a/frontend/src/app/main/ui/settings/sidebar.cljs +++ b/frontend/src/app/main/ui/settings/sidebar.cljs @@ -91,10 +91,11 @@ :on-click go-settings-profile} [:span {:class (stl/css :element-title)} (tr "labels.profile")]] - [:li {:class (stl/css-case :current password? - :settings-item true) - :on-click go-settings-password} - [:span {:class (stl/css :element-title)} (tr "labels.password")]] + (when-not (cf/auth-type-sso?) + [:li {:class (stl/css-case :current password? + :settings-item true) + :on-click go-settings-password} + [:span {:class (stl/css :element-title)} (tr "labels.password")]]) [:li {:class (stl/css-case :current notifications? :settings-item true)