♻️ Refactor auth code

This commit is contained in:
Andrey Antukh 2022-06-30 15:11:41 +02:00
parent d021ac0226
commit 14d1cb90bd
30 changed files with 1306 additions and 960 deletions

View file

@ -2,6 +2,26 @@
## :rocket: Next ## :rocket: Next
### :boom: Breaking changes & Deprecations
- The `PENPOT_LOGIN_WITH_LDAP` environment variable is finally removed (after
many version with deprecation). It is replaced with the
`enable-login-with-ldap` flag.
- The `PENPOT_LDAP_ATTRS_PHOTO` finally removed, it was unused for many
versions.
- If you are using social login (google, github, gitlab or generic OIDC) you
will need to ensure to add the following flags respectivelly to let them
enabled: `enable-login-with-google`, `enable-login-with-github`,
`enable-login-with-gitlab` and `enable-login-with-oidc`. If not, they will
remain disabled after application start independently if you set the client-id
and client-sectet options.
- The `PENPOT_REGISTRATION_ENABLED` is finally removed in favour of
`<enable|disable>-registration` flag.
- The OIDC providers are now initialized synchronously, and if you are using the
discovery mechanism of the generic OIDC integration, the start time of the
application will depend on how fast the OIDC provider responds to the
discovery http request.
### :sparkles: New features ### :sparkles: New features
- Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982) - Allow for nested and rotated boards inside other boards and groups [Taiga #2874](https://tree.taiga.io/project/penpot/us/2874?milestone=319982)

View file

@ -0,0 +1,137 @@
;; 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) UXBOX Labs SL
(ns app.auth.ldap
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cf]
[clj-ldap.client :as ldap]
[clojure.spec.alpha :as s]
[clojure.string]
[integrant.core :as ig]))
(defn- prepare-params
[cfg]
{:ssl? (:ssl cfg)
:startTLS? (:tls cfg)
:bind-dn (:bind-dn cfg)
:password (:bind-password cfg)
:host {:address (:host cfg)
:port (:port cfg)}})
(defn- connect
"Connects to the LDAP provider and returns a connection. An
exception is raised if no connection is possible."
^java.lang.AutoCloseable
[cfg]
(try
(-> cfg prepare-params ldap/connect)
(catch Throwable cause
(ex/raise :type :restriction
:code :unable-to-connect-to-ldap
:hint "unable to connect to ldap server"
:cause cause))))
(defn- replace-several [s & {:as replacements}]
(reduce-kv clojure.string/replace s replacements))
(defn- search-user
[{:keys [conn attrs base-dn] :as cfg} email]
(let [query (replace-several (:query cfg) ":username" email)
params {:filter query
:sizelimit 1
:attributes attrs}]
(first (ldap/search conn base-dn params))))
(defn- retrieve-user
[{:keys [conn] :as cfg} {:keys [email password]}]
(when-let [{:keys [dn] :as user} (search-user cfg email)]
(when (ldap/bind? conn dn password)
{:fullname (get user (-> cfg :attrs-fullname keyword))
:email email
:backend "ldap"})))
(s/def ::fullname ::us/not-empty-string)
(s/def ::email ::us/email)
(s/def ::backend ::us/not-empty-string)
(s/def ::info-data
(s/keys :req-un [::fullname ::email ::backend]))
(defn authenticate
[cfg params]
(with-open [conn (connect cfg)]
(when-let [user (-> (assoc cfg :conn conn)
(retrieve-user params))]
(when-not (s/valid? ::info-data user)
(let [explain (s/explain-str ::info-data user)]
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
(ex/raise :type :restriction
:code :wrong-ldap-response
:explain explain)))
user)))
(defn- try-connectivity
[cfg]
;; If we have ldap parameters, try to establish connection
(when (and (:bind-dn cfg)
(:bind-password cfg)
(:host cfg)
(:port cfg))
(try
(with-open [_ (connect cfg)]
(l/info :hint "provider initialized"
:provider "ldap"
:host (:host cfg)
:port (:port cfg)
:tls? (:tls cfg)
:ssl? (:ssl cfg)
:bind-dn (:bind-dn cfg)
:base-dn (:base-dn cfg)
:query (:query cfg))
cfg)
(catch Throwable cause
(l/error :hint "unable to connect to LDAP server (LDAP auth provider disabled)"
:host (:host cfg) :port (:port cfg) :cause cause)
nil))))
(defn- prepare-attributes
[cfg]
(assoc cfg :attrs [(:attrs-username cfg)
(:attrs-email cfg)
(:attrs-fullname cfg)]))
(defmethod ig/init-key ::provider
[_ cfg]
(when (:enabled? cfg)
(some-> cfg try-connectivity prepare-attributes)))
(s/def ::enabled? ::us/boolean)
(s/def ::host ::cf/ldap-host)
(s/def ::port ::cf/ldap-port)
(s/def ::ssl ::cf/ldap-ssl)
(s/def ::tls ::cf/ldap-starttls)
(s/def ::query ::cf/ldap-user-query)
(s/def ::base-dn ::cf/ldap-base-dn)
(s/def ::bind-dn ::cf/ldap-bind-dn)
(s/def ::bind-password ::cf/ldap-bind-password)
(s/def ::attrs-email ::cf/ldap-attrs-email)
(s/def ::attrs-fullname ::cf/ldap-attrs-fullname)
(s/def ::attrs-username ::cf/ldap-attrs-username)
(defmethod ig/pre-init-spec ::provider
[_]
(s/keys :opt-un [::host ::port
::ssl ::tls
::enabled?
::bind-dn
::bind-password
::query
::attrs-email
::attrs-username
::attrs-fullname]))

View file

@ -4,19 +4,23 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.http.oauth (ns app.auth.oidc
"OIDC client implementation."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http.middleware :as hmw]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.util.json :as json] [app.util.json :as json]
[app.util.time :as dt] [app.util.time :as dt]
[app.worker :as wrk]
[clojure.set :as set] [clojure.set :as set]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -25,6 +29,218 @@
[promesa.exec :as px] [promesa.exec :as px]
[yetti.response :as yrs])) [yetti.response :as yrs]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OIDC PROVIDER (GENERIC)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- discover-oidc-config
[{:keys [http-client]} {:keys [base-uri] :as opts}]
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration")
response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))]
(cond
(ex/exception? response)
(do
(l/warn :hint "unable to discover oidc configuration"
:discover-uri (str discovery-uri)
:cause response)
nil)
(= 200 (:status response))
(let [data (json/read (:body response))]
{:token-uri (get data :token_endpoint)
:auth-uri (get data :authorization_endpoint)
:user-uri (get data :userinfo_endpoint)})
:else
(do
(l/warn :hint "unable to discover OIDC configuration"
:uri (str discovery-uri)
:response-status-code (:status response))
nil))))
(defn- prepare-oidc-opts
[cfg]
(let [opts {:base-uri (:base-uri cfg)
:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:token-uri (:token-uri cfg)
:auth-uri (:auth-uri cfg)
:user-uri (:user-uri cfg)
:scopes (:scopes cfg #{"openid" "profile" "email"})
:roles-attr (:roles-attr cfg)
:roles (:roles cfg)
:name "oidc"}
opts (d/without-nils opts)]
(when (and (string? (:base-uri opts))
(string? (:client-id opts))
(string? (:client-secret opts)))
(if (and (string? (:token-uri opts))
(string? (:user-uri opts))
(string? (:auth-uri opts)))
opts
(some-> (discover-oidc-config cfg opts)
(merge opts {:discover? true}))))))
(defmethod ig/prep-key ::generic-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::generic-provider
[_ cfg]
(when (:enabled? cfg)
(if-let [opts (prepare-oidc-opts cfg)]
(do
(l/info :hint "provider initialized"
:provider :oidc
:method (if (:discover? opts) "discover" "manual")
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts))
:scopes (str/join "," (:scopes opts))
:auth-uri (:auth-uri opts)
:user-uri (:user-uri opts)
:token-uri (:token-uri opts)
:roles-attr (:roles-attr opts)
:roles (:roles opts))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :oidc)
nil))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GOOGLE AUTH PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/prep-key ::google-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::google-provider
[_ cfg]
(let [opts {:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:scopes #{"openid" "email" "profile"}
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
:token-uri "https://oauth2.googleapis.com/token"
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
:name "google"}]
(when (:enabled? cfg)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :google
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :google)
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GITHUB AUTH PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- retrieve-github-email
[{:keys [http-client]} tdata info]
(or (some-> info :email p/resolved)
(-> (http-client {:uri "https://api.github.com/user/emails"
:headers {"Authorization" (dm/str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get})
(p/then (fn [{:keys [status body] :as response}]
(when-not (s/int-in-range? 200 300 status)
(ex/raise :type :internal
:code :unable-to-retrieve-github-emails
:hint "unable to retrieve github emails"
:http-status status
:http-body body))
(->> response :body json/read (filter :primary) first :email))))))
(defmethod ig/prep-key ::github-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::github-provider
[_ cfg]
(let [opts {:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:scopes #{"read:user" "user:email"}
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:user-uri "https://api.github.com/user"
:name "github"
;; Additional hooks for provider specific way of
;; retrieve emails.
:get-email-fn (partial retrieve-github-email cfg)}]
(when (:enabled? cfg)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :github
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :github)
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GITLAB AUTH PROVIDER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/prep-key ::gitlab-provider
[_ cfg]
(d/without-nils cfg))
(defmethod ig/init-key ::gitlab-provider
[_ cfg]
(let [base (:base-uri cfg "https://gitlab.com")
opts {:base-uri base
:client-id (:client-id cfg)
:client-secret (:client-secret cfg)
:scopes #{"openid" "profile" "email"}
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/oauth/userinfo")
:name "gitlab"}]
(when (:enabled? cfg)
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :hint "provider initialized"
:provider :gitlab
:base-uri base
:client-id (:client-id opts)
:client-secret (obfuscate-string (:client-secret opts)))
opts)
(do
(l/warn :hint "unable to initialize auth provider, missing configuration" :provider :gitlab)
nil)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HANDLERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- build-redirect-uri (defn- build-redirect-uri
[{:keys [provider] :as cfg}] [{:keys [provider] :as cfg}]
(let [public (u/uri (:public-uri cfg))] (let [public (u/uri (:public-uri cfg))]
@ -81,42 +297,30 @@
:timeout 6000 :timeout 6000
:method :get})) :method :get}))
(retrieve-emails [] (validate-response [response]
(if (some? (:emails-uri provider)) (when-not (s/int-in-range? 200 300 (:status response))
(http-client {:uri (:emails-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000
:method :get})
(p/resolved {:status 200})))
(validate-response [[retrieve-res emails-res]]
(when-not (s/int-in-range? 200 300 (:status retrieve-res))
(ex/raise :type :internal (ex/raise :type :internal
:code :unable-to-retrieve-user-info :code :unable-to-retrieve-user-info
:hint "unable to retrieve user info" :hint "unable to retrieve user info"
:http-status (:status retrieve-res) :http-status (:status response)
:http-body (:body retrieve-res))) :http-body (:body response)))
(when-not (s/int-in-range? 200 300 (:status emails-res)) response)
(ex/raise :type :internal
:code :unable-to-retrieve-user-info
:hint "unable to retrieve user info"
:http-status (:status emails-res)
:http-body (:body emails-res)))
[retrieve-res emails-res])
(get-email [info] (get-email [info]
;; Allow providers hook into this for custom email
;; retrieval method.
(if-let [get-email-fn (:get-email-fn provider)]
(get-email-fn tdata info)
(let [attr-kw (cf/get :oidc-email-attr :email)] (let [attr-kw (cf/get :oidc-email-attr :email)]
(get info attr-kw))) (get info attr-kw))))
(get-name [info] (get-name [info]
(let [attr-kw (cf/get :oidc-name-attr :name)] (let [attr-kw (cf/get :oidc-name-attr :name)]
(get info attr-kw))) (get info attr-kw)))
(process-response [[retrieve-res emails-res]] (process-response [response]
(let [info (json/read (:body retrieve-res)) (p/let [info (-> response :body json/read)
email (if (some? (:extract-email-callback provider)) email (get-email info)]
((:extract-email-callback provider) emails-res)
(get-email info))]
{:backend (:name provider) {:backend (:name provider)
:email email :email email
:fullname (or (get-name info) email) :fullname (or (get-name info) email)
@ -133,10 +337,10 @@
:info info)) :info info))
info)] info)]
(-> (p/all [(retrieve) (retrieve-emails)]) (-> (retrieve)
(p/then' validate-response) (p/then validate-response)
(p/then' process-response) (p/then process-response)
(p/then' validate-info)))) (p/then validate-info))))
(s/def ::backend ::us/not-empty-string) (s/def ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string) (s/def ::email ::us/not-empty-string)
@ -195,8 +399,6 @@
(p/then' validate-oidc) (p/then' validate-oidc)
(p/then' (partial post-process state)))))) (p/then' (partial post-process state))))))
;; --- HTTP HANDLERS
(defn- retrieve-profile (defn- retrieve-profile
[{:keys [pool executor] :as cfg} info] [{:keys [pool executor] :as cfg} info]
(px/with-dispatch executor (px/with-dispatch executor
@ -256,8 +458,7 @@
(redirect-response uri)))) (redirect-response uri))))
(defn- auth-handler (defn- auth-handler
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise] [{:keys [tokens] :as cfg} {:keys [params] :as request}]
(try
(let [props (audit/extract-utm-params params) (let [props (audit/extract-utm-params params)
state (tokens :generate state (tokens :generate
{:iss :oauth {:iss :oauth
@ -265,12 +466,10 @@
:props props :props props
:exp (dt/in-future "15m")}) :exp (dt/in-future "15m")})
uri (build-auth-uri cfg state)] uri (build-auth-uri cfg state)]
(respond (yrs/response 200 {:redirect-uri uri}))) (yrs/response 200 {:redirect-uri uri})))
(catch Throwable cause
(raise cause))))
(defn- callback-handler (defn- callback-handler
[cfg request respond _] [cfg request]
(letfn [(process-request [] (letfn [(process-request []
(p/let [info (retrieve-info cfg request) (p/let [info (retrieve-info cfg request)
profile (retrieve-profile cfg info)] profile (retrieve-profile cfg info)]
@ -278,182 +477,62 @@
(handle-error [cause] (handle-error [cause]
(l/error :hint "error on oauth process" :cause cause) (l/error :hint "error on oauth process" :cause cause)
(respond (generate-error-redirect cfg cause)))] (generate-error-redirect cfg cause))]
(-> (process-request) (-> (process-request)
(p/then respond)
(p/catch handle-error)))) (p/catch handle-error))))
;; --- INIT (def provider-lookup
{:compile
(declare initialize) (fn [& _]
(fn [handler]
(fn [{:keys [providers] :as cfg} request]
(let [provider (some-> request :path-params :provider keyword)]
(if-let [provider (get providers provider)]
(handler (assoc cfg :provider provider) request)
(ex/raise :type :restriction
:code :provider-not-configured
:provider provider
:hint "provider not configured"))))))})
(s/def ::public-uri ::us/not-empty-string) (s/def ::public-uri ::us/not-empty-string)
(s/def ::http-client fn?)
(s/def ::session map?) (s/def ::session map?)
(s/def ::tokens fn?) (s/def ::tokens fn?)
(s/def ::rpc map?) (s/def ::providers map?)
(defmethod ig/pre-init-spec ::handler [_] (defmethod ig/pre-init-spec ::routes
(s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool])) [_]
(s/keys :req-un [::public-uri
::session
::tokens
::http-client
::providers
::db/pool
::wrk/executor]))
(defn wrap-handler (defmethod ig/init-key ::routes
[cfg handler] [_ {:keys [executor session] :as cfg}]
(fn [request respond raise] (let [cfg (update cfg :provider d/without-nils)]
(let [provider (get-in request [:path-params :provider]) ["" {:middleware [[(:middleware session)]
provider (get-in @cfg [:providers provider])] [hmw/with-promise-async executor]
(if provider [hmw/with-config cfg]
(handler (assoc @cfg :provider provider) [provider-lookup]
request ]}
respond ;; We maintain the both URI prefixes for backward compatibility.
raise)
(raise
(ex/error
:type :not-found
:provider provider
:hint "provider not configured"))))))
(defmethod ig/init-key ::handler ["/auth/oauth"
[_ cfg] ["/:provider"
(let [cfg (initialize cfg)] {:handler auth-handler
{:handler (wrap-handler cfg auth-handler) :allowed-methods #{:post}}]
:callback-handler (wrap-handler cfg callback-handler)})) ["/:provider/callback"
{:handler callback-handler
:allowed-methods #{:get}}]]
(defn- discover-oidc-config ["/auth/oidc"
[{:keys [http-client]} {:keys [base-uri] :as opts}] ["/:provider"
{:handler auth-handler
(let [discovery-uri (u/join base-uri ".well-known/openid-configuration") :allowed-methods #{:post}}]
response (ex/try (http-client {:method :get :uri (str discovery-uri)} {:sync? true}))] ["/:provider/callback"
(cond {:handler callback-handler
(ex/exception? response) :allowed-methods #{:get}}]]]))
(do
(l/warn :hint "unable to discover oidc configuration"
:discover-uri (str discovery-uri)
:cause response)
nil)
(= 200 (:status response))
(let [data (json/read (:body response))]
{:token-uri (get data :token_endpoint)
:auth-uri (get data :authorization_endpoint)
:user-uri (get data :userinfo_endpoint)})
:else
(do
(l/warn :hint "unable to discover OIDC configuration"
:uri (str discovery-uri)
:response-status-code (:status response))
nil))))
(defn- obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
(defn- initialize-oidc-provider
[cfg]
(let [opts {:base-uri (cf/get :oidc-base-uri)
:client-id (cf/get :oidc-client-id)
:client-secret (cf/get :oidc-client-secret)
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:scopes (cf/get :oidc-scopes #{"openid" "profile" "email"})
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)
:name "oidc"}]
(if (and (string? (:base-uri opts))
(string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/debug :hint "initialize oidc provider" :name "generic-oidc"
:opts (update opts :client-secret obfuscate-string))
(if (and (string? (:token-uri opts))
(string? (:user-uri opts))
(string? (:auth-uri opts)))
(do
(l/debug :hint "initialized with user provided configuration")
(assoc-in cfg [:providers "oidc"] opts))
(do
(l/debug :hint "trying to discover oidc provider configuration using BASE_URI")
(if-let [opts' (discover-oidc-config cfg opts)]
(do
(l/debug :hint "discovered opts" :additional-opts opts')
(assoc-in cfg [:providers "oidc"] (merge opts opts')))
cfg))))
cfg)))
(defn- initialize-google-provider
[cfg]
(let [opts {:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret)
:scopes #{"openid" "email" "profile"}
:auth-uri "https://accounts.google.com/o/oauth2/v2/auth"
:token-uri "https://oauth2.googleapis.com/token"
:user-uri "https://openidconnect.googleapis.com/v1/userinfo"
:name "google"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "google"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "google"] opts))
cfg)))
(defn extract-github-email
[response]
(let [emails (json/read (:body response))
primary-email (->> emails
(filter #(:primary %))
first)]
(:email primary-email)))
(defn- initialize-github-provider
[cfg]
(let [opts {:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)
:scopes #{"read:user" "user:email"}
:auth-uri "https://github.com/login/oauth/authorize"
:token-uri "https://github.com/login/oauth/access_token"
:emails-uri "https://api.github.com/user/emails"
:extract-email-callback extract-github-email
:user-uri "https://api.github.com/user"
:name "github"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "github"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "github"] opts))
cfg)))
(defn- initialize-gitlab-provider
[cfg]
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
opts {:base-uri base
:client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret)
:scopes #{"openid" "profile" "email"}
:auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token")
:user-uri (str base "/oauth/userinfo")
:name "gitlab"}]
(if (and (string? (:client-id opts))
(string? (:client-secret opts)))
(do
(l/info :action "initialize" :provider "gitlab"
:opts (pr-str (update opts :client-secret obfuscate-string)))
(assoc-in cfg [:providers "gitlab"] opts))
cfg)))
(defn- initialize
[cfg]
(let [cfg (agent cfg :error-mode :continue)]
(send-off cfg initialize-google-provider)
(send-off cfg initialize-gitlab-provider)
(send-off cfg initialize-github-provider)
(send-off cfg initialize-oidc-provider)
cfg))

View file

@ -10,6 +10,7 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.db :as db] [app.db :as db]
[app.main :as main] [app.main :as main]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.mutations.profile :as profile] [app.rpc.mutations.profile :as profile]
[app.rpc.queries.profile :refer [retrieve-profile-data-by-email]] [app.rpc.queries.profile :refer [retrieve-profile-data-by-email]]
[clojure.string :as str] [clojure.string :as str]
@ -54,13 +55,13 @@
:type :password}))] :type :password}))]
(try (try
(db/with-atomic [conn (:app.db/pool system)] (db/with-atomic [conn (:app.db/pool system)]
(->> (profile/create-profile conn (->> (cmd.auth/create-profile conn
{:fullname fullname {:fullname fullname
:email email :email email
:password password :password password
:is-active true :is-active true
:is-demo false}) :is-demo false})
(profile/create-profile-relations conn))) (cmd.auth/create-profile-relations conn)))
(when (pos? (:verbosity options)) (when (pos? (:verbosity options))
(println "User created successfully.")) (println "User created successfully."))

View file

@ -79,7 +79,6 @@
:ldap-attrs-username "uid" :ldap-attrs-username "uid"
:ldap-attrs-email "mail" :ldap-attrs-email "mail"
:ldap-attrs-fullname "cn" :ldap-attrs-fullname "cn"
:ldap-attrs-photo "jpegPhoto"
;; a server prop key where initial project is stored. ;; a server prop key where initial project is stored.
:initial-project-skey "initial-project"}) :initial-project-skey "initial-project"})
@ -149,7 +148,6 @@
(s/def ::initial-project-skey ::us/string) (s/def ::initial-project-skey ::us/string)
(s/def ::ldap-attrs-email ::us/string) (s/def ::ldap-attrs-email ::us/string)
(s/def ::ldap-attrs-fullname ::us/string) (s/def ::ldap-attrs-fullname ::us/string)
(s/def ::ldap-attrs-photo ::us/string)
(s/def ::ldap-attrs-username ::us/string) (s/def ::ldap-attrs-username ::us/string)
(s/def ::ldap-base-dn ::us/string) (s/def ::ldap-base-dn ::us/string)
(s/def ::ldap-bind-dn ::us/string) (s/def ::ldap-bind-dn ::us/string)
@ -256,7 +254,6 @@
::initial-project-skey ::initial-project-skey
::ldap-attrs-email ::ldap-attrs-email
::ldap-attrs-fullname ::ldap-attrs-fullname
::ldap-attrs-photo
::ldap-attrs-username ::ldap-attrs-username
::ldap-base-dn ::ldap-base-dn
::ldap-bind-dn ::ldap-bind-dn

View file

@ -9,7 +9,6 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.transit :as t] [app.common.transit :as t]
[app.http.doc :as doc]
[app.http.errors :as errors] [app.http.errors :as errors]
[app.http.middleware :as middleware] [app.http.middleware :as middleware]
[app.metrics :as mtx] [app.metrics :as mtx]
@ -67,8 +66,10 @@
:xnio/worker-threads (:worker-threads cfg) :xnio/worker-threads (:worker-threads cfg)
:xnio/dispatch (:executor cfg) :xnio/dispatch (:executor cfg)
:ring/async true} :ring/async true}
handler (if (some? router) handler (if (some? router)
(wrap-router router) (wrap-router router)
handler) handler)
server (yt/server handler (d/without-nils options))] server (yt/server handler (d/without-nils options))]
(assoc cfg :server (yt/start! server)))) (assoc cfg :server (yt/start! server))))
@ -113,7 +114,6 @@
;; HTTP ROUTER ;; HTTP ROUTER
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(s/def ::rpc map?)
(s/def ::oauth map?) (s/def ::oauth map?)
(s/def ::storage map?) (s/def ::storage map?)
(s/def ::assets map?) (s/def ::assets map?)
@ -122,15 +122,27 @@
(s/def ::audit-handler fn?) (s/def ::audit-handler fn?)
(s/def ::awsns-handler fn?) (s/def ::awsns-handler fn?)
(s/def ::session map?) (s/def ::session map?)
(s/def ::debug-routes vector?) (s/def ::rpc-routes (s/nilable vector?))
(s/def ::debug-routes (s/nilable vector?))
(s/def ::oidc-routes (s/nilable vector?))
(s/def ::doc-routes (s/nilable vector?))
(defmethod ig/pre-init-spec ::router [_] (defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::rpc ::mtx/metrics ::ws ::oauth ::storage ::assets (s/keys :req-un [::mtx/metrics
::session ::feedback ::awsns-handler ::debug-routes ::ws
::audit-handler])) ::storage
::assets
::session
::feedback
::awsns-handler
::debug-routes
::oidc-routes
::audit-handler
::rpc-routes
::doc-routes]))
(defmethod ig/init-key ::router (defmethod ig/init-key ::router
[_ {:keys [ws session rpc oauth metrics assets feedback debug-routes] :as cfg}] [_ {:keys [ws session metrics assets feedback] :as cfg}]
(rr/router (rr/router
[["" {:middleware [[middleware/server-timing] [["" {:middleware [[middleware/server-timing]
[middleware/format-response] [middleware/format-response]
@ -145,7 +157,7 @@
["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}] ["/by-file-media-id/:id" {:handler (:file-objects-handler assets)}]
["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]] ["/by-file-media-id/:id/thumbnail" {:handler (:file-thumbnails-handler assets)}]]
debug-routes (:debug-routes cfg)
["/webhooks" ["/webhooks"
["/sns" {:handler (:awsns-handler cfg) ["/sns" {:handler (:awsns-handler cfg)
@ -156,22 +168,12 @@
:allowed-methods #{:get}}] :allowed-methods #{:get}}]
["/api" {:middleware [[middleware/cors] ["/api" {:middleware [[middleware/cors]
(:middleware session)]} [(:middleware session)]]}
["/_doc" {:handler (doc/handler rpc)
:allowed-methods #{:get}}]
["/feedback" {:handler feedback
:allowed-methods #{:post}}]
["/auth/oauth/:provider" {:handler (:handler oauth)
:allowed-methods #{:post}}]
["/auth/oauth/:provider/callback" {:handler (:callback-handler oauth)
:allowed-methods #{:get}}]
["/audit/events" {:handler (:audit-handler cfg) ["/audit/events" {:handler (:audit-handler cfg)
:allowed-methods #{:post}}] :allowed-methods #{:post}}]
["/feedback" {:handler feedback
:allowed-methods #{:post}}]
(:doc-routes cfg)
(:oidc-routes cfg)
(:rpc-routes cfg)]]]))
["/rpc"
["/command/:command" {:handler (:command-handler rpc)}]
["/query/:type" {:handler (:query-handler rpc)}]
["/mutation/:type" {:handler (:mutation-handler rpc)
:allowed-methods #{:post}}]]]]]))

View file

@ -9,14 +9,16 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.config :as cf] [app.config :as cf]
[app.rpc :as-alias rpc]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.template :as tmpl] [app.util.template :as tmpl]
[clojure.java.io :as io] [clojure.java.io :as io]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig]
[pretty-spec.core :as ps] [pretty-spec.core :as ps]
[yetti.response :as yrs])) [yetti.response :as yrs]))
(defn get-spec-str (defn- get-spec-str
[k] [k]
(with-out-str (with-out-str
(ps/pprint (s/form k) (ps/pprint (s/form k)
@ -24,8 +26,8 @@
"clojure.core.specs.alpha" "score" "clojure.core.specs.alpha" "score"
"clojure.core" nil}}))) "clojure.core" nil}})))
(defn prepare-context (defn- prepare-context
[rpc] [methods]
(letfn [(gen-doc [type [name f]] (letfn [(gen-doc [type [name f]]
(let [mdata (meta f)] (let [mdata (meta f)]
;; (prn name mdata) ;; (prn name mdata)
@ -38,22 +40,32 @@
{:command-methods {:command-methods
(into [] (into []
(map (partial gen-doc :command)) (map (partial gen-doc :command))
(->> rpc :methods :command (sort-by first))) (->> methods :commands (sort-by first)))
:query-methods :query-methods
(into [] (into []
(map (partial gen-doc :query)) (map (partial gen-doc :query))
(->> rpc :methods :query (sort-by first))) (->> methods :queries (sort-by first)))
:mutation-methods :mutation-methods
(into [] (into []
(map (partial gen-doc :mutation)) (map (partial gen-doc :mutation))
(->> rpc :methods :mutation (sort-by first)))})) (->> methods :mutations (sort-by first)))}))
(defn handler (defn- handler
[rpc] [methods]
(let [context (prepare-context rpc)]
(if (contains? cf/flags :backend-api-doc) (if (contains? cf/flags :backend-api-doc)
(let [context (prepare-context methods)]
(fn [_ respond _] (fn [_ respond _]
(respond (yrs/response 200 (-> (io/resource "api-doc.tmpl") (respond (yrs/response 200 (-> (io/resource "api-doc.tmpl")
(tmpl/render context))))) (tmpl/render context))))))
(fn [_ respond _] (fn [_ respond _]
(respond (yrs/response 404)))))) (respond (yrs/response 404)))))
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req-un [::rpc/methods]))
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
["/_doc" {:handler (handler methods)
:allowed-methods #{:get}}])

View file

@ -143,13 +143,11 @@
(defn handle (defn handle
[cause request] [cause request]
(cond (cond
(or (instance? java.util.concurrent.CompletionException cause) (or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause)) (instance? java.util.concurrent.ExecutionException cause))
(handle-exception (.getCause ^Throwable cause) request) (handle-exception (.getCause ^Throwable cause) request)
(ex/wrapped? cause) (ex/wrapped? cause)
(let [context (meta cause) (let [context (meta cause)
cause (deref cause)] cause (deref cause)]

View file

@ -201,6 +201,7 @@
(fn [handler executor] (fn [handler executor]
(fn [request respond raise] (fn [request respond raise]
(-> (px/submit! executor #(handler request)) (-> (px/submit! executor #(handler request))
(p/bind p/wrap)
(p/then respond) (p/then respond)
(p/catch raise)))))}) (p/catch raise)))))})

View file

@ -6,6 +6,7 @@
(ns app.main (ns app.main
(:require (:require
[app.auth.oidc]
[app.common.logging :as l] [app.common.logging :as l]
[app.config :as cf] [app.config :as cf]
[app.util.time :as dt] [app.util.time :as dt]
@ -90,6 +91,9 @@
:app.http/session :app.http/session
{:store (ig/ref :app.http.session/store)} {:store (ig/ref :app.http.session/store)}
:app.http.doc/routes
{:methods (ig/ref :app.rpc/methods)}
:app.http.session/store :app.http.session/store
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens) :tokens (ig/ref :app.tokens/tokens)
@ -123,20 +127,81 @@
:max-body-size (cf/get :http-server-max-body-size) :max-body-size (cf/get :http-server-max-body-size)
:max-multipart-body-size (cf/get :http-server-max-multipart-body-size)} :max-multipart-body-size (cf/get :http-server-max-multipart-body-size)}
:app.auth.ldap/provider
{:host (cf/get :ldap-host)
:port (cf/get :ldap-port)
:ssl (cf/get :ldap-ssl)
:tls (cf/get :ldap-starttls)
:query (cf/get :ldap-user-query)
:attrs-email (cf/get :ldap-attrs-email)
:attrs-fullname (cf/get :ldap-attrs-fullname)
:attrs-username (cf/get :ldap-attrs-username)
:base-dn (cf/get :ldap-base-dn)
:bind-dn (cf/get :ldap-bind-dn)
:bind-password (cf/get :ldap-bind-password)
:enabled? (contains? cf/flags :login-with-ldap)}
:app.auth.oidc/google-provider
{:enabled? (contains? cf/flags :login-with-google)
:client-id (cf/get :google-client-id)
:client-secret (cf/get :google-client-secret)}
:app.auth.oidc/github-provider
{:enabled? (contains? cf/flags :login-with-github)
:http-client (ig/ref :app.http/client)
:client-id (cf/get :github-client-id)
:client-secret (cf/get :github-client-secret)}
:app.auth.oidc/gitlab-provider
{:enabled? (contains? cf/flags :login-with-gitlab)
:base-uri (cf/get :gitlab-base-uri "https://gitlab.com")
:client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret)}
:app.auth.oidc/generic-provider
{:enabled? (contains? cf/flags :login-with-oidc)
:http-client (ig/ref :app.http/client)
:client-id (cf/get :oidc-client-id)
:client-secret (cf/get :oidc-client-secret)
:base-uri (cf/get :oidc-base-uri)
:token-uri (cf/get :oidc-token-uri)
:auth-uri (cf/get :oidc-auth-uri)
:user-uri (cf/get :oidc-user-uri)
:scopes (cf/get :oidc-scopes)
:roles-attr (cf/get :oidc-roles-attr)
:roles (cf/get :oidc-roles)}
:app.auth.oidc/routes
{:providers {:google (ig/ref :app.auth.oidc/google-provider)
:github (ig/ref :app.auth.oidc/github-provider)
:gitlab (ig/ref :app.auth.oidc/gitlab-provider)
:oidc (ig/ref :app.auth.oidc/generic-provider)}
:tokens (ig/ref :app.tokens/tokens)
:http-client (ig/ref :app.http/client)
:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http/session)
:public-uri (cf/get :public-uri)
:executor (ig/ref [::default :app.worker/executor])}
:app.http/router :app.http/router
{:assets (ig/ref :app.http.assets/handlers) {:assets (ig/ref :app.http.assets/handlers)
:feedback (ig/ref :app.http.feedback/handler) :feedback (ig/ref :app.http.feedback/handler)
:session (ig/ref :app.http/session) :session (ig/ref :app.http/session)
:awsns-handler (ig/ref :app.http.awsns/handler) :awsns-handler (ig/ref :app.http.awsns/handler)
:oauth (ig/ref :app.http.oauth/handler)
:debug-routes (ig/ref :app.http.debug/routes) :debug-routes (ig/ref :app.http.debug/routes)
:oidc-routes (ig/ref :app.auth.oidc/routes)
:ws (ig/ref :app.http.websocket/handler) :ws (ig/ref :app.http.websocket/handler)
:metrics (ig/ref :app.metrics/metrics) :metrics (ig/ref :app.metrics/metrics)
:public-uri (cf/get :public-uri) :public-uri (cf/get :public-uri)
:storage (ig/ref :app.storage/storage) :storage (ig/ref :app.storage/storage)
:tokens (ig/ref :app.tokens/tokens) :tokens (ig/ref :app.tokens/tokens)
:audit-handler (ig/ref :app.loggers.audit/http-handler) :audit-handler (ig/ref :app.loggers.audit/http-handler)
:rpc (ig/ref :app.rpc/rpc) :rpc-routes (ig/ref :app.rpc/routes)
:doc-routes (ig/ref :app.http.doc/routes)
:executor (ig/ref [::default :app.worker/executor])} :executor (ig/ref [::default :app.worker/executor])}
:app.http.debug/routes :app.http.debug/routes
@ -162,17 +227,7 @@
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:executor (ig/ref [::default :app.worker/executor])} :executor (ig/ref [::default :app.worker/executor])}
:app.http.oauth/handler :app.rpc/methods
{:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http/session)
:pool (ig/ref :app.db/pool)
:tokens (ig/ref :app.tokens/tokens)
:audit (ig/ref :app.loggers.audit/collector)
:executor (ig/ref [::default :app.worker/executor])
:http-client (ig/ref :app.http/client)
:public-uri (cf/get :public-uri)}
:app.rpc/rpc
{:pool (ig/ref :app.db/pool) {:pool (ig/ref :app.db/pool)
:session (ig/ref :app.http/session) :session (ig/ref :app.http/session)
:tokens (ig/ref :app.tokens/tokens) :tokens (ig/ref :app.tokens/tokens)
@ -181,9 +236,13 @@
:msgbus (ig/ref :app.msgbus/msgbus) :msgbus (ig/ref :app.msgbus/msgbus)
:public-uri (cf/get :public-uri) :public-uri (cf/get :public-uri)
:audit (ig/ref :app.loggers.audit/collector) :audit (ig/ref :app.loggers.audit/collector)
:ldap (ig/ref :app.auth.ldap/provider)
:http-client (ig/ref :app.http/client) :http-client (ig/ref :app.http/client)
:executors (ig/ref :app.worker/executors)} :executors (ig/ref :app.worker/executors)}
:app.rpc/routes
{:methods (ig/ref :app.rpc/methods)}
:app.worker/worker :app.worker/worker
{:executor (ig/ref [::worker :app.worker/executor]) {:executor (ig/ref [::worker :app.worker/executor])
:tasks (ig/ref :app.worker/registry) :tasks (ig/ref :app.worker/registry)

View file

@ -223,15 +223,13 @@
(defn- resolve-mutation-methods (defn- resolve-mutation-methods
[cfg] [cfg]
(let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)] (let [cfg (assoc cfg ::type "mutation" ::metrics-id :rpc-mutation-timing)]
(->> (sv/scan-ns 'app.rpc.mutations.demo (->> (sv/scan-ns 'app.rpc.mutations.media
'app.rpc.mutations.media
'app.rpc.mutations.profile 'app.rpc.mutations.profile
'app.rpc.mutations.files 'app.rpc.mutations.files
'app.rpc.mutations.comments 'app.rpc.mutations.comments
'app.rpc.mutations.projects 'app.rpc.mutations.projects
'app.rpc.mutations.teams 'app.rpc.mutations.teams
'app.rpc.mutations.management 'app.rpc.mutations.management
'app.rpc.mutations.ldap
'app.rpc.mutations.fonts 'app.rpc.mutations.fonts
'app.rpc.mutations.share-link 'app.rpc.mutations.share-link
'app.rpc.mutations.verify-token) 'app.rpc.mutations.verify-token)
@ -241,26 +239,65 @@
(defn- resolve-command-methods (defn- resolve-command-methods
[cfg] [cfg]
(let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)] (let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns 'app.rpc.commands.binfile) (->> (sv/scan-ns 'app.rpc.commands.binfile
'app.rpc.commands.auth
'app.rpc.commands.ldap
'app.rpc.commands.demo)
(map (partial process-method cfg)) (map (partial process-method cfg))
(into {})))) (into {}))))
(s/def ::storage some?)
(s/def ::session map?)
(s/def ::tokens fn?)
(s/def ::audit (s/nilable fn?)) (s/def ::audit (s/nilable fn?))
(s/def ::executors (s/map-of keyword? ::wrk/executor)) (s/def ::executors (s/map-of keyword? ::wrk/executor))
(s/def ::executors map?)
(s/def ::http-client fn?)
(s/def ::ldap (s/nilable map?))
(s/def ::msgbus fn?)
(s/def ::public-uri ::us/not-empty-string)
(s/def ::session map?)
(s/def ::storage some?)
(s/def ::tokens fn?)
(defmethod ig/pre-init-spec ::rpc [_] (defmethod ig/pre-init-spec ::methods [_]
(s/keys :req-un [::storage ::session ::tokens ::audit (s/keys :req-un [::storage
::executors ::mtx/metrics ::db/pool])) ::session
::tokens
::audit
::executors
::public-uri
::msgbus
::http-client
::mtx/metrics
::db/pool
::ldap]))
(defmethod ig/init-key ::rpc (defmethod ig/init-key ::methods
[_ cfg] [_ cfg]
(let [mq (resolve-query-methods cfg) {:mutations (resolve-mutation-methods cfg)
mm (resolve-mutation-methods cfg) :queries (resolve-query-methods cfg)
cm (resolve-command-methods cfg)] :commands (resolve-command-methods cfg)})
{:methods {:query mq :mutation mm :command cm}
:command-handler (partial rpc-command-handler cm) (s/def ::mutations
:query-handler (partial rpc-query-handler mq) (s/map-of keyword? fn?))
:mutation-handler (partial rpc-mutation-handler mm)}))
(s/def ::queries
(s/map-of keyword? fn?))
(s/def ::commands
(s/map-of keyword? fn?))
(s/def ::methods
(s/keys :req-un [::mutations
::queries
::commands]))
(defmethod ig/pre-init-spec ::routes [_]
(s/keys :req-un [::methods]))
(defmethod ig/init-key ::routes
[_ {:keys [methods] :as cfg}]
[["/rpc"
["/command/:command" {:handler (partial rpc-command-handler (:commands methods))}]
["/query/:type" {:handler (partial rpc-query-handler (:queries methods))}]
["/mutation/:type" {:handler (partial rpc-mutation-handler (:mutations methods))
:allowed-methods #{:post}}]]])

View file

@ -0,0 +1,416 @@
;; 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) UXBOX Labs SL
(ns app.rpc.commands.auth
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.emails :as eml]
[app.loggers.audit :as audit]
[app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile]
[app.rpc.rlimit :as rlimit]
[app.util.services :as sv]
[app.util.time :as dt]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]))
(s/def ::email ::us/email)
(s/def ::fullname ::us/not-empty-string)
(s/def ::lang ::us/string)
(s/def ::path ::us/string)
(s/def ::profile-id ::us/uuid)
(s/def ::password ::us/not-empty-string)
(s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string)
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::token ::us/not-empty-string)
;; ---- HELPERS
(defn derive-password
[password]
(hashers/derive password
{:alg :argon2id
:memory 16384
:iterations 20
:parallelism 2}))
(defn verify-password
[attempt password]
(try
(hashers/verify attempt password)
(catch Exception _e
{:update false
:valid false})))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string."
[domains email]
(if (or (empty? domains)
(nil? domains))
true
(let [[_ candidate] (-> (str/lower email)
(str/split #"@" 2))]
(contains? domains candidate))))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = ?
and deleted_at is null) as val")
(defn check-profile-existence!
[conn {:keys [email] :as params}]
(let [email (str/lower email)
result (db/exec-one! conn [sql:profile-existence email])]
(when (:val result)
(ex/raise :type :validation
:code :email-already-exists))
params))
;; ---- COMMAND: login with password
(defn login-with-password
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
(when-not (contains? cf/flags :login)
(ex/raise :type :restriction
:code :login-disabled
:hint "login is disabled in this instance"))
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
(ex/raise :type :validation
:code :account-without-password
:hint "the current account does not have password"))
(:valid (verify-password password (:password profile))))
(validate-profile [profile]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :wrong-credentials))
(when-not profile
(ex/raise :type :validation
:code :wrong-credentials))
(when-not (check-password profile password)
(ex/raise :type :validation
:code :wrong-credentials))
profile)]
(db/with-atomic [conn pool]
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
(validate-profile)
(profile/strip-private-attrs)
(profile/populate-additional-data conn)
(profile/decode-profile-row))
invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
;; invitation because invitations matches exactly; and user can't loging with other email and
;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)}
profile)]
(with-meta response
{:transform-response ((:create session) (:id profile))
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
(s/def ::login-with-password
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-password
"Performs authentication using penpot password."
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[cfg params]
(login-with-password cfg params))
;; ---- COMMAND: Logout
(s/def ::logout
(s/keys :opt-un [::profile-id]))
(sv/defmethod ::logout
"Clears the authentication cookie and logout the current session."
{:auth false}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))
;; ---- COMMAND: Recover Profile
(defn recover-profile
[{:keys [pool tokens] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token]
(let [tdata (tokens :verify {:token token :iss :password-recovery})]
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (derive-password password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool]
(->> (validate-token token)
(update-password conn))
nil)))
(s/def ::token ::us/not-empty-string)
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[cfg params]
(recover-profile cfg params))
;; ---- COMMAND: Prepare Register
(defn prepare-register
[{:keys [pool tokens] :as cfg} params]
(when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)
(let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
:hint "email should match the invitation")))))
(when-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params))
(ex/raise :type :validation
:code :email-domain-is-not-allowed)))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spammer.
(when (eml/has-bounce-reports? pool (:email params))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email has one or many bounces reported"))
(check-profile-existence! pool params)
(when (= (str/lower (:email params))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
(let [params {:email (:email params)
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:exp (dt/in-future "48h")}
token (tokens :generate params)]
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
(s/def ::prepare-register-profile
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::prepare-register-profile {:auth false}
[cfg params]
(prepare-register cfg params))
;; ---- COMMAND: Register Profile
(defn create-profile
"Create the profile entry on the database with limited input filling
all the other fields with defaults."
[conn params]
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
(db/tjson))
password (if-let [password (:password params)]
(derive-password password)
"!")
locale (:locale params)
locale (when (and (string? locale) (not (str/blank? locale)))
locale)
backend (:backend params "penpot")
is-demo (:is-demo params false)
is-muted (:is-muted params false)
is-active (:is-active params false)
email (str/lower (:email params))
params {:id id
:fullname (:fullname params)
:email email
:auth-backend backend
:lang locale
:password password
:deleted-at (:deleted-at params)
:props props
:is-active is-active
:is-muted is-muted
:is-demo is-demo}]
(try
(-> (db/insert! conn :profile params)
(profile/decode-profile-row))
(catch org.postgresql.util.PSQLException e
(let [state (.getSQLState e)]
(if (not= state "23505")
(throw e)
(ex/raise :type :validation
:code :email-already-exists
:cause e)))))))
(defn create-profile-relations
[conn profile]
(let [team (teams/create-team conn {:profile-id (:id profile)
:name "Default"
:is-default true})]
(-> profile
(profile/strip-private-attrs)
(assoc :default-team-id (:id team))
(assoc :default-project-id (:default-project-id team)))))
(defn register-profile
[{:keys [conn tokens session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens :verify {:token token :iss :prepared-register})
params (merge params claims)]
(check-profile-existence! conn params)
(let [is-active (or (:is-active params)
(contains? cf/flags :insecure-register))
profile (->> (assoc params :is-active is-active)
(create-profile conn)
(create-profile-relations conn)
(profile/decode-profile-row))
invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))]
(cond
;; If invitation token comes in params, this is because the user comes from team-invitation process;
;; in this case, regenerate token and send back to the user a new invitation token (and mark current
;; session as logged). This happens only if the invitation email matches with the register email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens :generate claims)
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
;; If auth backend is different from "penpot" means user is
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; In all other cases, send a verification email.
:else
(let [vtoken (tokens :generate
{:iss :verify-email
:exp (dt/in-future "48h")
:profile-id (:id profile)
:email (:email profile)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(eml/send! {::eml/conn conn
::eml/factory eml/register
:public-uri (:public-uri cfg)
:to (:email profile)
:name (:fullname profile)
:token vtoken
:extra-data ptoken})
(with-meta profile
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(register-profile params))))
;; ---- COMMAND: Request Profile Recovery
(defn request-profile-recovery
[{:keys [pool tokens] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens :generate
{:iss :password-recovery
:exp (dt/in-future "15m")
:profile-id id})]
(assoc profile :token token)))
(send-email-notification [conn profile]
(let [ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(eml/send! {::eml/conn conn
::eml/factory eml/password-recovery
:public-uri (:public-uri cfg)
:to (:email profile)
:token (:token profile)
:name (:fullname profile)
:extra-data ptoken})
nil))]
(db/with-atomic [conn pool]
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
(when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(when-not (:is-active profile)
(ex/raise :type :validation
:code :profile-not-verified
:hint "the user need to validate profile before recover password"))
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(->> profile
(create-recovery-token)
(send-email-notification conn))))))
(s/def ::request-profile-recovery
(s/keys :req-un [::email]))
(sv/defmethod ::request-profile-recovery {:auth false}
[cfg params]
(request-profile-recovery cfg params))

View file

@ -4,7 +4,7 @@
;; ;;
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.rpc.mutations.demo (ns app.rpc.commands.demo
"A demo specific mutations." "A demo specific mutations."
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
@ -12,7 +12,7 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.rpc.mutations.profile :as profile] [app.rpc.commands.auth :as cmd.auth]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
@ -45,8 +45,8 @@
:hint "Demo users are disabled by config.")) :hint "Demo users are disabled by config."))
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(->> (#'profile/create-profile conn params) (->> (cmd.auth/create-profile conn params)
(#'profile/create-profile-relations conn)) (cmd.auth/create-profile-relations conn))
(with-meta {:email email (with-meta {:email email
:password password} :password password}

View file

@ -0,0 +1,75 @@
;; 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) UXBOX Labs SL
(ns app.rpc.commands.ldap
(:require
[app.auth.ldap :as ldap]
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.db :as db]
[app.loggers.audit :as-alias audit]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.queries.profile :as profile]
[app.util.services :as sv]
[clojure.spec.alpha :as s]))
;; --- COMMAND: login-with-ldap
(declare login-or-register)
(s/def ::email ::us/email)
(s/def ::password ::us/string)
(s/def ::invitation-token ::us/string)
(s/def ::login-with-ldap
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-ldap {:auth false}
[{:keys [session tokens ldap] :as cfg} params]
(when-not ldap
(ex/raise :type :restriction
:code :ldap-not-initialized
:hide "ldap auth provider is not initialized"))
(let [info (ldap/authenticate ldap params)]
(when-not info
(ex/raise :type :validation
:code :wrong-credentials))
(let [profile (login-or-register cfg info)]
(if-let [token (:invitation-token params)]
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged).
(let [claims (tokens :verify {:token token :iss :team-invitation})
claims (assoc claims
:member-id (:id profile)
:member-email (:email profile))
token (tokens :generate claims)]
(with-meta {:invitation-token token}
{:transform-response ((:create session) (:id profile))
::audit/props (:props profile)
::audit/profile-id (:id profile)}))
(with-meta profile
{:transform-response ((:create session) (:id profile))
::audit/props (:props profile)
::audit/profile-id (:id profile)})))))
(defn- login-or-register
[{:keys [pool] :as cfg} info]
(db/with-atomic [conn pool]
(or (some->> (:email info)
(profile/retrieve-profile-data-by-email conn)
(profile/populate-additional-data conn)
(profile/decode-profile-row))
(->> (assoc info :is-active true :is-demo false)
(cmd.auth/create-profile conn)
(cmd.auth/create-profile-relations conn)
(profile/strip-private-attrs)))))

View file

@ -1,141 +0,0 @@
;; 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) UXBOX Labs SL
(ns app.rpc.mutations.ldap
(:require
[app.common.exceptions :as ex]
[app.common.logging :as l]
[app.common.spec :as us]
[app.config :as cfg]
[app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.mutations.profile :as profile-m]
[app.rpc.queries.profile :as profile-q]
[app.util.services :as sv]
[clj-ldap.client :as ldap]
[clojure.spec.alpha :as s]
[clojure.string]))
(s/def ::fullname ::us/not-empty-string)
(s/def ::email ::us/email)
(s/def ::backend ::us/not-empty-string)
(s/def ::info-data
(s/keys :req-un [::fullname ::email ::backend]))
(defn connect
^java.lang.AutoCloseable
[]
(let [params {:ssl? (cfg/get :ldap-ssl)
:startTLS? (cfg/get :ldap-starttls)
:bind-dn (cfg/get :ldap-bind-dn)
:password (cfg/get :ldap-bind-password)
:host {:address (cfg/get :ldap-host)
:port (cfg/get :ldap-port)}}]
(try
(ldap/connect params)
(catch Exception e
(ex/raise :type :restriction
:code :ldap-disabled
:hint "ldap disabled or unable to connect"
:cause e)))))
;; --- Mutation: login-with-ldap
(declare authenticate)
(declare login-or-register)
(s/def ::email ::us/email)
(s/def ::password ::us/string)
(s/def ::invitation-token ::us/string)
(s/def ::login-with-ldap
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::login-with-ldap {:auth false}
[{:keys [pool session tokens] :as cfg} params]
(db/with-atomic [conn pool]
(let [info (authenticate params)
cfg (assoc cfg :conn conn)]
(when-not info
(ex/raise :type :validation
:code :wrong-credentials))
(when-not (s/valid? ::info-data info)
(let [explain (s/explain-str ::info-data info)]
(l/warn ::l/raw (str "invalid response from ldap, looks like ldap is not configured correctly\n" explain))
(ex/raise :type :restriction
:code :wrong-ldap-response
:reason explain)))
(let [profile (login-or-register cfg {:email (:email info)
:backend (:backend info)
:fullname (:fullname info)})]
(if-let [token (:invitation-token params)]
;; If invitation token comes in params, this is because the
;; user comes from team-invitation process; in this case,
;; regenerate token and send back to the user a new invitation
;; token (and mark current session as logged).
(let [claims (tokens :verify {:token token :iss :team-invitation})
claims (assoc claims
:member-id (:id profile)
:member-email (:email profile))
token (tokens :generate claims)]
(with-meta {:invitation-token token}
{:transform-response ((:create session) (:id profile))
::audit/props (:props profile)
::audit/profile-id (:id profile)}))
(with-meta profile
{:transform-response ((:create session) (:id profile))
::audit/props (:props profile)
::audit/profile-id (:id profile)}))))))
(defn- replace-several [s & {:as replacements}]
(reduce-kv clojure.string/replace s replacements))
(defn- get-ldap-user
[cpool {:keys [email] :as params}]
(let [query (-> (cfg/get :ldap-user-query)
(replace-several ":username" email))
attrs [(cfg/get :ldap-attrs-username)
(cfg/get :ldap-attrs-email)
(cfg/get :ldap-attrs-photo)
(cfg/get :ldap-attrs-fullname)]
base-dn (cfg/get :ldap-base-dn)
params {:filter query
:sizelimit 1
:attributes attrs}]
(first (ldap/search cpool base-dn params))))
(defn- authenticate
[{:keys [password email] :as params}]
(with-open [conn (connect)]
(when-let [{:keys [dn] :as luser} (get-ldap-user conn params)]
(when (ldap/bind? conn dn password)
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
:email email
:backend "ldap"}))))
(defn- login-or-register
[{:keys [conn] :as cfg} info]
(or (some->> (:email info)
(profile-q/retrieve-profile-data-by-email conn)
(profile-q/populate-additional-data conn)
(profile-q/decode-profile-row))
(let [params (-> info
(assoc :is-active true)
(assoc :is-demo false))]
(->> params
(profile-m/create-profile conn)
(profile-m/create-profile-relations conn)
(profile-q/strip-private-attrs)))))

View file

@ -9,19 +9,18 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.emails :as eml] [app.emails :as eml]
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.media :as media] [app.media :as media]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.mutations.teams :as teams] [app.rpc.mutations.teams :as teams]
[app.rpc.queries.profile :as profile] [app.rpc.queries.profile :as profile]
[app.rpc.rlimit :as rlimit] [app.rpc.rlimit :as rlimit]
[app.storage :as sto] [app.storage :as sto]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.hashers :as hashers]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str] [cuerdas.core :as str]
[promesa.core :as p] [promesa.core :as p]
@ -37,310 +36,6 @@
(s/def ::password ::us/not-empty-string) (s/def ::password ::us/not-empty-string)
(s/def ::old-password ::us/not-empty-string) (s/def ::old-password ::us/not-empty-string)
(s/def ::theme ::us/string) (s/def ::theme ::us/string)
(s/def ::invitation-token ::us/not-empty-string)
(declare check-profile-existence!)
(declare create-profile)
(declare create-profile-relations)
(declare register-profile)
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if
given whitelist is an empty string."
[domains email]
(if (or (empty? domains)
(nil? domains))
true
(let [[_ candidate] (-> (str/lower email)
(str/split #"@" 2))]
(contains? domains candidate))))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = ?
and deleted_at is null) as val")
(defn check-profile-existence!
[conn {:keys [email] :as params}]
(let [email (str/lower email)
result (db/exec-one! conn [sql:profile-existence email])]
(when (:val result)
(ex/raise :type :validation
:code :email-already-exists))
params))
(defn derive-password
[password]
(hashers/derive password
{:alg :argon2id
:memory 16384
:iterations 20
:parallelism 2}))
(defn verify-password
[attempt password]
(try
(hashers/verify attempt password)
(catch Exception _e
{:update false
:valid false})))
(defn decode-profile-row
[{:keys [props] :as profile}]
(cond-> profile
(db/pgobject? props "jsonb")
(assoc :props (db/decode-transit-pgobject props))))
;; --- MUTATION: Prepare Register
(s/def ::prepare-register-profile
(s/keys :req-un [::email ::password]
:opt-un [::invitation-token]))
(sv/defmethod ::prepare-register-profile {:auth false}
[{:keys [pool tokens] :as cfg} params]
(when-not (contains? cf/flags :registration)
(if-not (contains? params :invitation-token)
(ex/raise :type :restriction
:code :registration-disabled)
(let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})]
(when-not (= (:email params) (:member-email invitation))
(ex/raise :type :restriction
:code :email-does-not-match-invitation
:hint "email should match the invitation")))))
(when-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params))
(ex/raise :type :validation
:code :email-domain-is-not-allowed)))
;; Don't allow proceed in preparing registration if the profile is
;; already reported as spammer.
(when (eml/has-bounce-reports? pool (:email params))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email has one or many bounces reported"))
(check-profile-existence! pool params)
(when (= (str/lower (:email params))
(str/lower (:password params)))
(ex/raise :type :validation
:code :email-as-password
:hint "you can't use your email as password"))
(let [params {:email (:email params)
:password (:password params)
:invitation-token (:invitation-token params)
:backend "penpot"
:iss :prepared-register
:exp (dt/in-future "48h")}
token (tokens :generate params)]
(with-meta {:token token}
{::audit/profile-id uuid/zero})))
;; --- MUTATION: Register Profile
(s/def ::token ::us/not-empty-string)
(s/def ::register-profile
(s/keys :req-un [::token ::fullname]))
(sv/defmethod ::register-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(register-profile params))))
(defn register-profile
[{:keys [conn tokens session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens :verify {:token token :iss :prepared-register})
params (merge params claims)]
(check-profile-existence! conn params)
(let [is-active (or (:is-active params)
(contains? cf/flags :insecure-register))
profile (->> (assoc params :is-active is-active)
(create-profile conn)
(create-profile-relations conn)
(decode-profile-row))
invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))]
(cond
;; If invitation token comes in params, this is because the user comes from team-invitation process;
;; in this case, regenerate token and send back to the user a new invitation token (and mark current
;; session as logged). This happens only if the invitation email matches with the register email.
(and (some? invitation) (= (:email profile) (:member-email invitation)))
(let [claims (assoc invitation :member-id (:id profile))
token (tokens :generate claims)
resp {:invitation-token token}]
(with-meta resp
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))
;; If auth backend is different from "penpot" means user is
;; registering using third party auth mechanism; in this case
;; we need to mark this session as logged.
(not= "penpot" (:auth-backend profile))
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; If the `:enable-insecure-register` flag is set, we proceed
;; to sign in the user directly, without email verification.
(true? is-active)
(with-meta (profile/strip-private-attrs profile)
{:transform-response ((:create session) (:id profile))
::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)})
;; In all other cases, send a verification email.
:else
(let [vtoken (tokens :generate
{:iss :verify-email
:exp (dt/in-future "48h")
:profile-id (:id profile)
:email (:email profile)})
ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(eml/send! {::eml/conn conn
::eml/factory eml/register
:public-uri (:public-uri cfg)
:to (:email profile)
:name (:fullname profile)
:token vtoken
:extra-data ptoken})
(with-meta profile
{::audit/replace-props (audit/profile->props profile)
::audit/profile-id (:id profile)}))))))
(defn create-profile
"Create the profile entry on the database with limited input filling
all the other fields with defaults."
[conn params]
(let [id (or (:id params) (uuid/next))
props (-> (audit/extract-utm-params params)
(merge (:props params))
(db/tjson))
password (if-let [password (:password params)]
(derive-password password)
"!")
locale (:locale params)
locale (when (and (string? locale) (not (str/blank? locale)))
locale)
backend (:backend params "penpot")
is-demo (:is-demo params false)
is-muted (:is-muted params false)
is-active (:is-active params false)
email (str/lower (:email params))
params {:id id
:fullname (:fullname params)
:email email
:auth-backend backend
:lang locale
:password password
:deleted-at (:deleted-at params)
:props props
:is-active is-active
:is-muted is-muted
:is-demo is-demo}]
(try
(-> (db/insert! conn :profile params)
(decode-profile-row))
(catch org.postgresql.util.PSQLException e
(let [state (.getSQLState e)]
(if (not= state "23505")
(throw e)
(ex/raise :type :validation
:code :email-already-exists
:cause e)))))))
(defn create-profile-relations
[conn profile]
(let [team (teams/create-team conn {:profile-id (:id profile)
:name "Default"
:is-default true})]
(-> profile
(profile/strip-private-attrs)
(assoc :default-team-id (:id team))
(assoc :default-project-id (:default-project-id team)))))
;; --- MUTATION: Login
(s/def ::email ::us/email)
(s/def ::scope ::us/string)
(s/def ::login
(s/keys :req-un [::email ::password]
:opt-un [::scope ::invitation-token]))
(sv/defmethod ::login
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
(when-not (contains? cf/flags :login)
(ex/raise :type :restriction
:code :login-disabled
:hint "login is disabled in this instance"))
(letfn [(check-password [profile password]
(when (= (:password profile) "!")
(ex/raise :type :validation
:code :account-without-password))
(:valid (verify-password password (:password profile))))
(validate-profile [profile]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :wrong-credentials))
(when-not profile
(ex/raise :type :validation
:code :wrong-credentials))
(when-not (check-password profile password)
(ex/raise :type :validation
:code :wrong-credentials))
profile)]
(db/with-atomic [conn pool]
(let [profile (->> (profile/retrieve-profile-data-by-email conn email)
(validate-profile)
(profile/strip-private-attrs)
(profile/populate-additional-data conn)
(decode-profile-row))
invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))
;; If invitation member-id does not matches the profile-id, we just proceed to ignore the
;; invitation because invitations matches exactly; and user can't loging with other email and
;; accept invitation with other email
response (if (and (some? invitation) (= (:id profile) (:member-id invitation)))
{:invitation-token (:invitation-token params)}
profile)]
(with-meta response
{:transform-response ((:create session) (:id profile))
::audit/props (audit/profile->props profile)
::audit/profile-id (:id profile)})))))
;; --- MUTATION: Logout
(s/def ::logout
(s/keys :opt-un [::profile-id]))
(sv/defmethod ::logout {:auth false}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))
;; --- MUTATION: Update Profile (own) ;; --- MUTATION: Update Profile (own)
@ -414,7 +109,7 @@
(defn- validate-password! (defn- validate-password!
[conn {:keys [profile-id old-password] :as params}] [conn {:keys [profile-id old-password] :as params}]
(let [profile (db/get-by-id conn :profile profile-id)] (let [profile (db/get-by-id conn :profile profile-id)]
(when-not (:valid (verify-password old-password (:password profile))) (when-not (:valid (cmd.auth/verify-password old-password (:password profile)))
(ex/raise :type :validation (ex/raise :type :validation
:code :old-password-not-match)) :code :old-password-not-match))
profile)) profile))
@ -422,7 +117,7 @@
(defn update-profile-password! (defn update-profile-password!
[conn {:keys [id password] :as profile}] [conn {:keys [id password] :as profile}]
(db/update! conn :profile (db/update! conn :profile
{:password (derive-password password)} {:password (cmd.auth/derive-password password)}
{:id id})) {:id id}))
;; --- MUTATION: Update Photo ;; --- MUTATION: Update Photo
@ -481,7 +176,7 @@
(defn- change-email-immediately (defn- change-email-immediately
[{:keys [conn]} {:keys [profile email] :as params}] [{:keys [conn]} {:keys [profile email] :as params}]
(when (not= email (:email profile)) (when (not= email (:email profile))
(check-profile-existence! conn params)) (cmd.auth/check-profile-existence! conn params))
(db/update! conn :profile (db/update! conn :profile
{:email email} {:email email}
{:id (:id profile)}) {:id (:id profile)})
@ -499,7 +194,7 @@
:profile-id (:id profile)})] :profile-id (:id profile)})]
(when (not= email (:email profile)) (when (not= email (:email profile))
(check-profile-existence! conn params)) (cmd.auth/check-profile-existence! conn params))
(when-not (eml/allow-send-emails? conn profile) (when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation (ex/raise :type :validation
@ -526,76 +221,6 @@
[conn id] [conn id]
(db/get-by-id conn :profile id {:for-update true})) (db/get-by-id conn :profile id {:for-update true}))
;; --- MUTATION: Request Profile Recovery
(s/def ::request-profile-recovery
(s/keys :req-un [::email]))
(sv/defmethod ::request-profile-recovery {:auth false}
[{:keys [pool tokens] :as cfg} {:keys [email] :as params}]
(letfn [(create-recovery-token [{:keys [id] :as profile}]
(let [token (tokens :generate
{:iss :password-recovery
:exp (dt/in-future "15m")
:profile-id id})]
(assoc profile :token token)))
(send-email-notification [conn profile]
(let [ptoken (tokens :generate-predefined
{:iss :profile-identity
:profile-id (:id profile)})]
(eml/send! {::eml/conn conn
::eml/factory eml/password-recovery
:public-uri (:public-uri cfg)
:to (:email profile)
:token (:token profile)
:name (:fullname profile)
:extra-data ptoken})
nil))]
(db/with-atomic [conn pool]
(when-let [profile (profile/retrieve-profile-data-by-email conn email)]
(when-not (eml/allow-send-emails? conn profile)
(ex/raise :type :validation
:code :profile-is-muted
:hint "looks like the profile has reported repeatedly as spam or has permanent bounces."))
(when-not (:is-active profile)
(ex/raise :type :validation
:code :profile-not-verified
:hint "the user need to validate profile before recover password"))
(when (eml/has-bounce-reports? conn (:email profile))
(ex/raise :type :validation
:code :email-has-permanent-bounces
:hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce"))
(->> profile
(create-recovery-token)
(send-email-notification conn))))))
;; --- MUTATION: Recover Profile
(s/def ::token ::us/not-empty-string)
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(sv/defmethod ::recover-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool tokens] :as cfg} {:keys [token password]}]
(letfn [(validate-token [token]
(let [tdata (tokens :verify {:token token :iss :password-recovery})]
(:profile-id tdata)))
(update-password [conn profile-id]
(let [pwd (derive-password password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))]
(db/with-atomic [conn pool]
(->> (validate-token token)
(update-password conn))
nil)))
;; --- MUTATION: Update Profile Props ;; --- MUTATION: Update Profile Props
@ -668,3 +293,61 @@
:code :owner-teams-with-people :code :owner-teams-with-people
:hint "The user need to transfer ownership of owned teams." :hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :team-id rows)})))) :context {:teams (mapv :team-id rows)}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; DEPRECATED METHODS (TO BE REMOVED ON 1.16.x)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- MUTATION: Login
(s/def ::login ::cmd.auth/login-with-password)
(sv/defmethod ::login
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[cfg params]
(cmd.auth/login-with-password cfg params))
;; --- MUTATION: Logout
(s/def ::logout ::cmd.auth/logout)
(sv/defmethod ::logout {:auth false}
[{:keys [session] :as cfg} _]
(with-meta {}
{:transform-response (:delete session)}))
;; --- MUTATION: Recover Profile
(s/def ::recover-profile ::cmd.auth/recover-profile)
(sv/defmethod ::recover-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[cfg params]
(cmd.auth/recover-profile cfg params))
;; --- MUTATION: Prepare Register
(s/def ::prepare-register-profile ::cmd.auth/prepare-register-profile)
(sv/defmethod ::prepare-register-profile {:auth false}
[cfg params]
(cmd.auth/prepare-register cfg params))
;; --- MUTATION: Register Profile
(s/def ::register-profile ::cmd.auth/register-profile)
(sv/defmethod ::register-profile
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(cmd.auth/register-profile params))))
;; --- MUTATION: Request Profile Recovery
(s/def ::request-profile-recovery ::cmd.auth/request-profile-recovery)
(sv/defmethod ::request-profile-recovery {:auth false}
[cfg params]
(cmd.auth/request-profile-recovery cfg params))

View file

@ -3,7 +3,7 @@
(:require (:require
[app.db :as db] [app.db :as db]
[app.config :as cfg] [app.config :as cfg]
[app.rpc.mutations.profile :refer [derive-password]] [app.rpc.commands.auth :refer [derive-password]]
[app.main :refer [system]])) [app.main :refer [system]]))
(defn reset-passwords (defn reset-passwords

View file

@ -46,7 +46,13 @@
(t/is (sto/storage-object? mobj1)) (t/is (sto/storage-object? mobj1))
(t/is (sto/storage-object? mobj2)) (t/is (sto/storage-object? mobj2))
(t/is (= 122785 (:size mobj1))) (t/is (= 122785 (:size mobj1)))
(t/is (= 3303 (:size mobj2)))))
;; This is because in ubuntu 21.04 generates different
;; thumbnail that in ubuntu 22.04. This hack should be removed
;; when we all use the ubuntu 22.04 devenv image.
(t/is (or
(= 3302 (:size mobj2))
(= 3303 (:size mobj2))))))
)) ))
(t/deftest media-object-upload (t/deftest media-object-upload

View file

@ -10,6 +10,7 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.rpc.mutations.profile :as profile] [app.rpc.mutations.profile :as profile]
[app.rpc.commands.auth :as cauth]
[app.test-helpers :as th] [app.test-helpers :as th]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.java.io :as io] [clojure.java.io :as io]
@ -27,11 +28,10 @@
;; Test with wrong credentials ;; Test with wrong credentials
(t/deftest profile-login-failed-1 (t/deftest profile-login-failed-1
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
data {::th/type :login data {::th/type :login-with-password
:email "profile1.test@nodomain.com" :email "profile1.test@nodomain.com"
:password "foobar" :password "foobar"}
:scope "foobar"} out (th/command! data)]
out (th/mutation! data)]
#_(th/print-result! out) #_(th/print-result! out)
(let [error (:error out)] (let [error (:error out)]
@ -42,11 +42,10 @@
;; Test with good credentials but profile not activated. ;; Test with good credentials but profile not activated.
(t/deftest profile-login-failed-2 (t/deftest profile-login-failed-2
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
data {::th/type :login data {::th/type :login-with-password
:email "profile1.test@nodomain.com" :email "profile1.test@nodomain.com"
:password "123123" :password "123123"}
:scope "foobar"} out (th/command! data)]
out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(let [error (:error out)] (let [error (:error out)]
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
@ -58,8 +57,7 @@
(let [profile (th/create-profile* 1 {:is-active true}) (let [profile (th/create-profile* 1 {:is-active true})
data {::th/type :login data {::th/type :login
:email "profile1.test@nodomain.com" :email "profile1.test@nodomain.com"
:password "123123" :password "123123"}
:scope "foobar"}
out (th/mutation! data)] out (th/mutation! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
@ -161,11 +159,11 @@
(t/deftest registration-domain-whitelist (t/deftest registration-domain-whitelist
(let [whitelist #{"gmail.com" "hey.com" "ya.ru"}] (let [whitelist #{"gmail.com" "hey.com" "ya.ru"}]
(t/testing "allowed email domain" (t/testing "allowed email domain"
(t/is (true? (profile/email-domain-in-whitelist? whitelist "username@ya.ru"))) (t/is (true? (cauth/email-domain-in-whitelist? whitelist "username@ya.ru")))
(t/is (true? (profile/email-domain-in-whitelist? #{} "username@somedomain.com")))) (t/is (true? (cauth/email-domain-in-whitelist? #{} "username@somedomain.com"))))
(t/testing "not allowed email domain" (t/testing "not allowed email domain"
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) (t/is (false? (cauth/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
(t/deftest prepare-register-and-register-profile (t/deftest prepare-register-and-register-profile
(let [data {::th/type :prepare-register-profile (let [data {::th/type :prepare-register-profile

View file

@ -9,14 +9,15 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.flags :as flags] [app.common.flags :as flags]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.pprint :as pp]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.pprint :as pp]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.main :as main] [app.main :as main]
[app.media] [app.media]
[app.migrations] [app.migrations]
[app.rpc.commands.auth :as cmd.auth]
[app.rpc.mutations.files :as files] [app.rpc.mutations.files :as files]
[app.rpc.mutations.profile :as profile] [app.rpc.mutations.profile :as profile]
[app.rpc.mutations.projects :as projects] [app.rpc.mutations.projects :as projects]
@ -31,8 +32,8 @@
[expound.alpha :as expound] [expound.alpha :as expound]
[integrant.core :as ig] [integrant.core :as ig]
[mockery.core :as mk] [mockery.core :as mk]
[yetti.request :as yrq] [promesa.core :as p]
[promesa.core :as p]) [yetti.request :as yrq])
(:import org.postgresql.ds.PGSimpleDataSource)) (:import org.postgresql.ds.PGSimpleDataSource))
(def ^:dynamic *system* nil) (def ^:dynamic *system* nil)
@ -59,10 +60,12 @@
:app.http/router :app.http/router
:app.http.awsns/handler :app.http.awsns/handler
:app.http.session/updater :app.http.session/updater
:app.http.oauth/google :app.auth.oidc/google-provider
:app.http.oauth/gitlab :app.auth.oidc/gitlab-provider
:app.http.oauth/github :app.auth.oidc/github-provider
:app.http.oauth/all :app.auth.oidc/generic-provider
:app.auth.oidc/routes
;; :app.auth.ldap/provider
:app.worker/executors-monitor :app.worker/executors-monitor
:app.http.oauth/handler :app.http.oauth/handler
:app.notifications/handler :app.notifications/handler
@ -81,9 +84,9 @@
(try (try
(binding [*system* system (binding [*system* system
*pool* (:app.db/pool system)] *pool* (:app.db/pool system)]
(mk/with-mocks [mock1 {:target 'app.rpc.mutations.profile/derive-password (mk/with-mocks [mock1 {:target 'app.rpc.commands.auth/derive-password
:return identity} :return identity}
mock2 {:target 'app.rpc.mutations.profile/verify-password mock2 {:target 'app.rpc.commands.auth/verify-password
:return (fn [a b] {:valid (= a b)})}] :return (fn [a b] {:valid (= a b)})}]
(next))) (next)))
(finally (finally
@ -140,8 +143,8 @@
:is-demo false} :is-demo false}
params)] params)]
(->> params (->> params
(#'profile/create-profile conn) (cmd.auth/create-profile conn)
(#'profile/create-profile-relations conn))))) (cmd.auth/create-profile-relations conn)))))
(defn create-project* (defn create-project*
([i params] (create-project* *pool* i params)) ([i params] (create-project* *pool* i params))
@ -267,17 +270,21 @@
{:error (handle-error e#) {:error (handle-error e#)
:result nil}))) :result nil})))
(defn command!
[{:keys [::type] :as data}]
(let [method-fn (get-in *system* [:app.rpc/methods :commands type])]
;; (app.common.pprint/pprint (:app.rpc/methods *system*))
(try-on! (method-fn (dissoc data ::type)))))
(defn mutation! (defn mutation!
[{:keys [::type] :as data}] [{:keys [::type] :as data}]
(let [method-fn (get-in *system* [:app.rpc/rpc :methods :mutation type])] (let [method-fn (get-in *system* [:app.rpc/methods :mutations type])]
(try-on! (try-on! (method-fn (dissoc data ::type)))))
(method-fn (dissoc data ::type)))))
(defn query! (defn query!
[{:keys [::type] :as data}] [{:keys [::type] :as data}]
(let [method-fn (get-in *system* [:app.rpc/rpc :methods :query type])] (let [method-fn (get-in *system* [:app.rpc/methods :queries type])]
(try-on! (try-on! (method-fn (dissoc data ::type)))))
(method-fn (dissoc data ::type)))))
;; --- UTILS ;; --- UTILS

View file

@ -97,4 +97,3 @@ PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com
# PENPOT_LDAP_ATTRS_USERNAME=uid # PENPOT_LDAP_ATTRS_USERNAME=uid
# PENPOT_LDAP_ATTRS_EMAIL=mail # PENPOT_LDAP_ATTRS_EMAIL=mail
# PENPOT_LDAP_ATTRS_FULLNAME=cn # PENPOT_LDAP_ATTRS_FULLNAME=cn
# PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto

View file

@ -4,69 +4,10 @@ log() {
echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $*" echo "[$(date +%Y-%m-%dT%H:%M:%S%:z)] $*"
} }
######################################### #########################################
## App Frontend config ## App Frontend config
######################################### #########################################
update_google_client_id() {
if [ -n "$PENPOT_GOOGLE_CLIENT_ID" ]; then
log "Updating Google Client Id: $PENPOT_GOOGLE_CLIENT_ID"
sed -i \
-e "s|^//var penpotGoogleClientID = \".*\";|var penpotGoogleClientID = \"$PENPOT_GOOGLE_CLIENT_ID\";|g" \
"$1"
fi
}
update_gitlab_client_id() {
if [ -n "$PENPOT_GITLAB_CLIENT_ID" ]; then
log "Updating GitLab Client Id: $PENPOT_GITLAB_CLIENT_ID"
sed -i \
-e "s|^//var penpotGitlabClientID = \".*\";|var penpotGitlabClientID = \"$PENPOT_GITLAB_CLIENT_ID\";|g" \
"$1"
fi
}
update_github_client_id() {
if [ -n "$PENPOT_GITHUB_CLIENT_ID" ]; then
log "Updating GitHub Client Id: $PENPOT_GITHUB_CLIENT_ID"
sed -i \
-e "s|^//var penpotGithubClientID = \".*\";|var penpotGithubClientID = \"$PENPOT_GITHUB_CLIENT_ID\";|g" \
"$1"
fi
}
update_oidc_client_id() {
if [ -n "$PENPOT_OIDC_CLIENT_ID" ]; then
log "Updating Oidc Client Id: $PENPOT_OIDC_CLIENT_ID"
sed -i \
-e "s|^//var penpotOIDCClientID = \".*\";|var penpotOIDCClientID = \"$PENPOT_OIDC_CLIENT_ID\";|g" \
"$1"
fi
}
# DEPRECATED
update_login_with_ldap() {
if [ -n "$PENPOT_LOGIN_WITH_LDAP" ]; then
log "Updating Login with LDAP: $PENPOT_LOGIN_WITH_LDAP"
sed -i \
-e "s|^//var penpotLoginWithLDAP = .*;|var penpotLoginWithLDAP = $PENPOT_LOGIN_WITH_LDAP;|g" \
"$1"
fi
}
# DEPRECATED
update_registration_enabled() {
if [ -n "$PENPOT_REGISTRATION_ENABLED" ]; then
log "Updating Registration Enabled: $PENPOT_REGISTRATION_ENABLED"
sed -i \
-e "s|^//var penpotRegistrationEnabled = .*;|var penpotRegistrationEnabled = $PENPOT_REGISTRATION_ENABLED;|g" \
"$1"
fi
}
update_flags() { update_flags() {
if [ -n "$PENPOT_FLAGS" ]; then if [ -n "$PENPOT_FLAGS" ]; then
sed -i \ sed -i \
@ -75,11 +16,5 @@ update_flags() {
fi fi
} }
update_google_client_id /var/www/app/js/config.js
update_gitlab_client_id /var/www/app/js/config.js
update_github_client_id /var/www/app/js/config.js
update_oidc_client_id /var/www/app/js/config.js
update_login_with_ldap /var/www/app/js/config.js
update_registration_enabled /var/www/app/js/config.js
update_flags /var/www/app/js/config.js update_flags /var/www/app/js/config.js
exec "$@"; exec "$@";

View file

@ -80,10 +80,6 @@
(def default-theme "default") (def default-theme "default")
(def default-language "en") (def default-language "en")
(def google-client-id (obj/get global "penpotGoogleClientID" nil))
(def gitlab-client-id (obj/get global "penpotGitlabClientID" nil))
(def github-client-id (obj/get global "penpotGithubClientID" nil))
(def oidc-client-id (obj/get global "penpotOIDCClientID" nil))
(def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js"))
(def translations (obj/get global "penpotTranslations")) (def translations (obj/get global "penpotTranslations"))
(def themes (obj/get global "penpotThemes")) (def themes (obj/get global "penpotThemes"))
@ -100,14 +96,6 @@
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil)) (def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil)) (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
;; maintain for backward compatibility
(let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false)
registration (obj/get global "penpotRegistrationEnabled" true)]
(when login-with-ldap
(swap! flags conj :login-with-ldap))
(when (false? registration)
(swap! flags disj :registration)))
(defn get-public-uri (defn get-public-uri
[] []
(let [uri (u/uri (or (obj/get global "penpotPublicURI") (let [uri (u/uri (or (obj/get global "penpotPublicURI")

View file

@ -145,7 +145,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(when (= status "ended") (when (= status "ended")
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id resource-id}) (->> (rp/command! :export {:cmd :get-resource :blob? true :id resource-id})
(rx/delay 500) (rx/delay 500)
(rx/map #(dom/trigger-download filename %))))))) (rx/map #(dom/trigger-download filename %)))))))
@ -165,9 +165,9 @@
:wait true}] :wait true}]
(rx/concat (rx/concat
(rx/of ::dwp/force-persist) (rx/of ::dwp/force-persist)
(->> (rp/query! :exporter params) (->> (rp/command! :export params)
(rx/mapcat (fn [{:keys [id filename]}] (rx/mapcat (fn [{:keys [id filename]}]
(->> (rp/query! :exporter {:cmd :get-resource :blob? true :id id}) (->> (rp/command! :export {:cmd :get-resource :blob? true :id id})
(rx/map (fn [data] (rx/map (fn [data]
(dom/trigger-download filename data) (dom/trigger-download filename data)
(clear-export-state uuid/zero)))))) (clear-export-state uuid/zero))))))
@ -213,7 +213,7 @@
;; Launch the exportation process and stores the resource id ;; Launch the exportation process and stores the resource id
;; locally. ;; locally.
(->> (rp/query! :exporter params) (->> (rp/command! :export params)
(rx/map (fn [{:keys [id] :as resource}] (rx/map (fn [{:keys [id] :as resource}]
(vreset! resource-id id) (vreset! resource-id id)
(initialize-export-status exports cmd resource)))) (initialize-export-status exports cmd resource))))

View file

@ -206,7 +206,7 @@
;; the returned profile is an NOT authenticated profile, we ;; the returned profile is an NOT authenticated profile, we
;; proceed to logout and show an error message. ;; proceed to logout and show an error message.
(->> (rp/mutation :login (d/without-nils params)) (->> (rp/command :login-with-password (d/without-nils params))
(rx/merge-map (fn [data] (rx/merge-map (fn [data]
(rx/merge (rx/merge
(rx/of (fetch-profile)) (rx/of (fetch-profile))
@ -292,7 +292,7 @@
(ptk/reify ::logout (ptk/reify ::logout
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/mutation :logout) (->> (rp/command :logout)
(rx/delay-at-least 300) (rx/delay-at-least 300)
(rx/catch (constantly (rx/of 1))) (rx/catch (constantly (rx/of 1)))
(rx/map #(logged-out params))))))) (rx/map #(logged-out params)))))))
@ -494,7 +494,7 @@
:or {on-error rx/throw :or {on-error rx/throw
on-success identity}} (meta data)] on-success identity}} (meta data)]
(->> (rp/mutation :request-profile-recovery data) (->> (rp/command :request-profile-recovery data)
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
@ -513,7 +513,7 @@
(let [{:keys [on-error on-success] (let [{:keys [on-error on-success]
:or {on-error rx/throw :or {on-error rx/throw
on-success identity}} (meta data)] on-success identity}} (meta data)]
(->> (rp/mutation :recover-profile data) (->> (rp/command :recover-profile data)
(rx/tap on-success) (rx/tap on-success)
(rx/catch on-error)))))) (rx/catch on-error))))))
@ -524,7 +524,7 @@
(ptk/reify ::create-demo-profile (ptk/reify ::create-demo-profile
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/mutation :create-demo-profile {}) (->> (rp/command :create-demo-profile {})
(rx/map login))))) (rx/map login)))))

View file

@ -73,10 +73,22 @@
(rx/map http/conditional-decode-transit) (rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))) (rx/mapcat handle-response)))
(defn- send-command!
"A simple helper for a common case of sending and receiving transit
data to the penpot mutation api."
[id params]
(->> (http/send! {:method :post
:uri (u/join base-uri "api/rpc/command/" (name id))
:credentials "include"
:body (http/transit-data params)})
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))
(defn- dispatch [& args] (first args)) (defn- dispatch [& args] (first args))
(defmulti query dispatch) (defmulti query dispatch)
(defmulti mutation dispatch) (defmulti mutation dispatch)
(defmulti command dispatch)
(defmethod query :default (defmethod query :default
[id params] [id params]
@ -90,6 +102,10 @@
[id params] [id params]
(send-mutation! id params)) (send-mutation! id params))
(defmethod command :default
[id params]
(send-command! id params))
(defn query! (defn query!
([id] (query id {})) ([id] (query id {}))
([id params] (query id params))) ([id params] (query id params)))
@ -98,7 +114,11 @@
([id] (mutation id {})) ([id] (mutation id {}))
([id params] (mutation id params))) ([id params] (mutation id params)))
(defmethod mutation :login-with-oauth (defn command!
([id] (command id {}))
([id params] (command id params)))
(defmethod command :login-with-oidc
[_ {:keys [provider] :as params}] [_ {:keys [provider] :as params}]
(let [uri (u/join base-uri "api/auth/oauth/" (d/name provider)) (let [uri (u/join base-uri "api/auth/oauth/" (d/name provider))
params (dissoc params :provider)] params (dissoc params :provider)]
@ -109,7 +129,7 @@
(rx/map http/conditional-decode-transit) (rx/map http/conditional-decode-transit)
(rx/mapcat handle-response)))) (rx/mapcat handle-response))))
(defmethod mutation :send-feedback (defmethod command :send-feedback
[_ params] [_ params]
(->> (http/send! {:method :post (->> (http/send! {:method :post
:uri (u/join base-uri "api/feedback") :uri (u/join base-uri "api/feedback")
@ -128,7 +148,7 @@
(rx/map http/conditional-decode-transit) (rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))) (rx/mapcat handle-response)))
(defmethod query :exporter (defmethod command :export
[_ params] [_ params]
(let [default {:wait false :blob? false}] (let [default {:wait false :blob? false}]
(send-export (merge default params)))) (send-export (merge default params))))

View file

@ -23,10 +23,11 @@
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(def show-alt-login-buttons? (def show-alt-login-buttons?
(or cf/google-client-id (some (partial contains? @cf/flags)
cf/gitlab-client-id [:login-with-google
cf/github-client-id :login-with-github
cf/oidc-client-id)) :login-with-gitlab
:login-with-oidc]))
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string) (s/def ::password ::us/not-empty-string)
@ -36,19 +37,27 @@
(s/keys :req-un [::email ::password] (s/keys :req-un [::email ::password]
:opt-un [::invitation-token])) :opt-un [::invitation-token]))
(defn- login-with-oauth (defn- login-with-oidc
[event provider params] [event provider params]
(dom/prevent-default event) (dom/prevent-default event)
(->> (rp/mutation! :login-with-oauth (assoc params :provider provider)) (->> (rp/command! :login-with-oidc (assoc params :provider provider))
(rx/subs (fn [{:keys [redirect-uri] :as rsp}] (rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(.replace js/location redirect-uri))))) (.replace js/location redirect-uri))
(fn [{:keys [type code] :as error}]
(cond
(and (= type :restriction)
(= code :provider-not-configured))
(st/emit! (dm/error (tr "errors.auth-provider-not-configured")))
:else
(st/emit! (dm/error (tr "errors.generic"))))))))
(defn- login-with-ldap (defn- login-with-ldap
[event params] [event params]
(dom/prevent-default event) (dom/prevent-default event)
(dom/stop-propagation event) (dom/stop-propagation event)
(let [{:keys [on-error]} (meta params)] (let [{:keys [on-error]} (meta params)]
(->> (rp/mutation! :login-with-ldap params) (->> (rp/command! :login-with-ldap params)
(rx/subs (fn [profile] (rx/subs (fn [profile]
(if-let [token (:invitation-token profile)] (if-let [token (:invitation-token profile)]
(st/emit! (rt/nav :auth-verify-token {} {:token token})) (st/emit! (rt/nav :auth-verify-token {} {:token token}))
@ -56,11 +65,15 @@
(fn [{:keys [type code] :as error}] (fn [{:keys [type code] :as error}]
(cond (cond
(and (= type :restriction) (and (= type :restriction)
(= code :ldap-disabled)) (= code :ldap-not-initialized))
(st/emit! (dm/error (tr "errors.ldap-disabled"))) (st/emit! (dm/error (tr "errors.ldap-disabled")))
(fn? on-error) (fn? on-error)
(on-error error))))))) (on-error error)
:else
(st/emit! (dm/error (tr "errors.generic")))))))))
(mf/defc login-form (mf/defc login-form
[{:keys [params] :as props}] [{:keys [params] :as props}]
@ -134,35 +147,35 @@
(mf/defc login-buttons (mf/defc login-buttons
[{:keys [params] :as props}] [{:keys [params] :as props}]
[:div.auth-buttons [:div.auth-buttons
(when cf/google-client-id (when (contains? @cf/flags :login-with-google)
[:a.btn-primary.btn-large.btn-google-auth [:a.btn-primary.btn-large.btn-google-auth
{:on-click #(login-with-oauth % :google params)} {:on-click #(login-with-oidc % :google params)}
[:span.logo i/brand-google] [:span.logo i/brand-google]
(tr "auth.login-with-google-submit")]) (tr "auth.login-with-google-submit")])
(when cf/github-client-id (when (contains? @cf/flags :login-with-github)
[:a.btn-primary.btn-large.btn-github-auth [:a.btn-primary.btn-large.btn-github-auth
{:on-click #(login-with-oauth % :github params)} {:on-click #(login-with-oidc % :github params)}
[:span.logo i/brand-github] [:span.logo i/brand-github]
(tr "auth.login-with-github-submit")]) (tr "auth.login-with-github-submit")])
(when cf/gitlab-client-id (when (contains? @cf/flags :login-with-gitlab)
[:a.btn-primary.btn-large.btn-gitlab-auth [:a.btn-primary.btn-large.btn-gitlab-auth
{:on-click #(login-with-oauth % :gitlab params)} {:on-click #(login-with-oidc % :gitlab params)}
[:span.logo i/brand-gitlab] [:span.logo i/brand-gitlab]
(tr "auth.login-with-gitlab-submit")]) (tr "auth.login-with-gitlab-submit")])
(when cf/oidc-client-id (when (contains? @cf/flags :login-with-oidc)
[:a.btn-primary.btn-large.btn-github-auth [:a.btn-primary.btn-large.btn-github-auth
{:on-click #(login-with-oauth % :oidc params)} {:on-click #(login-with-oidc % :oidc params)}
[:span.logo i/brand-openid] [:span.logo i/brand-openid]
(tr "auth.login-with-oidc-submit")])]) (tr "auth.login-with-oidc-submit")])])
(mf/defc login-button-oidc (mf/defc login-button-oidc
[{:keys [params] :as props}] [{:keys [params] :as props}]
(when cf/oidc-client-id (when (contains? @cf/flags :login-with-oidc)
[:div.link-entry.link-oidc [:div.link-entry.link-oidc
[:a {:on-click #(login-with-oauth % :oidc params)} [:a {:on-click #(login-with-oidc % :oidc params)}
(tr "auth.login-with-oidc-submit")]])) (tr "auth.login-with-oidc-submit")]]))
(mf/defc login-page (mf/defc login-page

View file

@ -84,7 +84,7 @@
(fn [form _event] (fn [form _event]
(reset! submitted? true) (reset! submitted? true)
(let [cdata (:clean-data @form)] (let [cdata (:clean-data @form)]
(->> (rp/mutation :prepare-register-profile cdata) (->> (rp/command :prepare-register-profile cdata)
(rx/map #(merge % params)) (rx/map #(merge % params))
(rx/finalize #(reset! submitted? false)) (rx/finalize #(reset! submitted? false))
(rx/subs (partial handle-prepare-register-success form) (rx/subs (partial handle-prepare-register-success form)
@ -207,7 +207,7 @@
(fn [form _event] (fn [form _event]
(reset! submitted? true) (reset! submitted? true)
(let [params (:clean-data @form)] (let [params (:clean-data @form)]
(->> (rp/mutation :register-profile params) (->> (rp/command :register-profile params)
(rx/finalize #(reset! submitted? false)) (rx/finalize #(reset! submitted? false))
(rx/subs (partial handle-register-success form) (rx/subs (partial handle-register-success form)
(partial handle-register-error form)))))) (partial handle-register-error form))))))

View file

@ -55,7 +55,7 @@
(fn [form _] (fn [form _]
(reset! loading true) (reset! loading true)
(let [data (:clean-data @form)] (let [data (:clean-data @form)]
(->> (rp/mutation! :send-feedback data) (->> (rp/command! :send-feedback data)
(rx/subs on-succes on-error)))))] (rx/subs on-succes on-error)))))]
[:& fm/form {:class "feedback-form" [:& fm/form {:class "feedback-form"

View file

@ -699,6 +699,10 @@ msgstr "This invite might be canceled or may be expired."
msgid "errors.ldap-disabled" msgid "errors.ldap-disabled"
msgstr "LDAP authentication is disabled." msgstr "LDAP authentication is disabled."
#: src/app/main/ui/auth/login.cljs
msgid "errors.auth-provider-not-configured"
msgstr "Authentication provider not configured."
msgid "errors.media-format-unsupported" msgid "errors.media-format-unsupported"
msgstr "The image format is not supported (must be svg, jpg or png)." msgstr "The image format is not supported (must be svg, jpg or png)."