♻️ Refactor LDAP auth backend.

And reorganize oauth backend namespaces.
This commit is contained in:
Andrey Antukh 2021-02-18 14:07:13 +01:00 committed by Andrés Moya
parent 299b29b66f
commit de394a7d4e
26 changed files with 288 additions and 310 deletions

View file

@ -15,14 +15,15 @@
### :bug: Bugs fixed ### :bug: Bugs fixed
- Add more improvements to french translation strings [#591](https://github.com/penpot/penpot/pull/591)
- Add some missing database indexes (mainly improves performance on large databases on file-update rpc method, and some background tasks). - Add some missing database indexes (mainly improves performance on large databases on file-update rpc method, and some background tasks).
- Fix corner cases on invitation/signup flows.
- Fix problem width handoff code generation [Taiga #1204](https://tree.taiga.io/project/penpot/issue/1204) - Fix problem width handoff code generation [Taiga #1204](https://tree.taiga.io/project/penpot/issue/1204)
- Fix problem with indices refreshing on page changes [#646](https://github.com/penpot/penpot/issues/646) - Fix problem with indices refreshing on page changes [#646](https://github.com/penpot/penpot/issues/646)
- Have language change notification written in the new language [Taiga #1205](https://tree.taiga.io/project/penpot/issue/1205) - Have language change notification written in the new language [Taiga #1205](https://tree.taiga.io/project/penpot/issue/1205)
- Properly handle errors on github, gitlab and ldap auth backends. - Properly handle errors on github, gitlab and ldap auth backends.
- Properly mark profile auth backend (on first register/ auth with 3rd party auth provider). - Properly mark profile auth backend (on first register/ auth with 3rd party auth provider).
- Fix corner cases on invitation/signup flows. - Refactor LDAP auth backend.
- Add more improvements to french translation strings [#591](https://github.com/penpot/penpot/pull/591)
### :heart: Community contributions by (Thank you!) ### :heart: Community contributions by (Thank you!)

View file

@ -70,20 +70,11 @@
:telemetry-enabled false :telemetry-enabled false
:telemetry-uri "https://telemetry.penpot.app/" :telemetry-uri "https://telemetry.penpot.app/"
;; LDAP auth disabled by default. Set ldap-auth-host to enable :ldap-user-query "(|(uid=$username)(mail=$username))"
;:ldap-auth-host "ldap.mysupercompany.com" :ldap-attrs-username "uid"
;:ldap-auth-port 389 :ldap-attrs-email "mail"
;:ldap-bind-dn "cn=admin,dc=ldap,dc=mysupercompany,dc=com" :ldap-attrs-fullname "cn"
;:ldap-bind-password "verysecure" :ldap-attrs-photo "jpegPhoto"
;:ldap-auth-ssl false
;:ldap-auth-starttls false
;:ldap-auth-base-dn "ou=People,dc=ldap,dc=mysupercompany,dc=com"
:ldap-auth-user-query "(|(uid=$username)(mail=$username))"
:ldap-auth-username-attribute "uid"
:ldap-auth-email-attribute "mail"
:ldap-auth-fullname-attribute "displayName"
:ldap-auth-avatar-attribute "jpegPhoto"
;; :initial-data-file "resources/initial-data.json" ;; :initial-data-file "resources/initial-data.json"
;; :initial-data-project-name "Penpot Oboarding" ;; :initial-data-project-name "Penpot Oboarding"
@ -152,18 +143,18 @@
(s/def ::github-client-id ::us/string) (s/def ::github-client-id ::us/string)
(s/def ::github-client-secret ::us/string) (s/def ::github-client-secret ::us/string)
(s/def ::ldap-auth-host ::us/string) (s/def ::ldap-host ::us/string)
(s/def ::ldap-auth-port ::us/integer) (s/def ::ldap-port ::us/integer)
(s/def ::ldap-bind-dn ::us/string) (s/def ::ldap-bind-dn ::us/string)
(s/def ::ldap-bind-password ::us/string) (s/def ::ldap-bind-password ::us/string)
(s/def ::ldap-auth-ssl ::us/boolean) (s/def ::ldap-ssl ::us/boolean)
(s/def ::ldap-auth-starttls ::us/boolean) (s/def ::ldap-starttls ::us/boolean)
(s/def ::ldap-auth-base-dn ::us/string) (s/def ::ldap-base-dn ::us/string)
(s/def ::ldap-auth-user-query ::us/string) (s/def ::ldap-user-query ::us/string)
(s/def ::ldap-auth-username-attribute ::us/string) (s/def ::ldap-attrs-username ::us/string)
(s/def ::ldap-auth-email-attribute ::us/string) (s/def ::ldap-attrs-email ::us/string)
(s/def ::ldap-auth-fullname-attribute ::us/string) (s/def ::ldap-attrs-fullname ::us/string)
(s/def ::ldap-auth-avatar-attribute ::us/string) (s/def ::ldap-attrs-photo ::us/string)
(s/def ::telemetry-enabled ::us/boolean) (s/def ::telemetry-enabled ::us/boolean)
(s/def ::telemetry-with-taiga ::us/boolean) (s/def ::telemetry-with-taiga ::us/boolean)
@ -195,18 +186,18 @@
::google-client-secret ::google-client-secret
::http-server-port ::http-server-port
::host ::host
::ldap-auth-avatar-attribute ::ldap-attrs-username
::ldap-auth-base-dn ::ldap-attrs-email
::ldap-auth-email-attribute ::ldap-attrs-fullname
::ldap-auth-fullname-attribute ::ldap-attrs-photo
::ldap-auth-host
::ldap-auth-port
::ldap-auth-ssl
::ldap-auth-starttls
::ldap-auth-user-query
::ldap-auth-username-attribute
::ldap-bind-dn ::ldap-bind-dn
::ldap-bind-password ::ldap-bind-password
::ldap-base-dn
::ldap-host
::ldap-port
::ldap-ssl
::ldap-starttls
::ldap-user-query
::public-uri ::public-uri
::profile-complaint-threshold ::profile-complaint-threshold
::profile-bounce-threshold ::profile-bounce-threshold

View file

@ -80,14 +80,12 @@
(s/def ::rpc map?) (s/def ::rpc map?)
(s/def ::session map?) (s/def ::session map?)
(s/def ::metrics map?) (s/def ::metrics map?)
(s/def ::google-auth map?) (s/def ::oauth map?)
(s/def ::gitlab-auth map?)
(s/def ::ldap-auth fn?)
(s/def ::storage map?) (s/def ::storage map?)
(s/def ::assets map?) (s/def ::assets map?)
(defmethod ig/pre-init-spec ::router [_] (defmethod ig/pre-init-spec ::router [_]
(s/keys :req-un [::rpc ::session ::metrics ::google-auth ::gitlab-auth ::storage ::assets])) (s/keys :req-un [::rpc ::session ::metrics ::oauth ::storage ::assets]))
(defmethod ig/init-key ::router (defmethod ig/init-key ::router
[_ cfg] [_ cfg]
@ -113,7 +111,7 @@
:body "internal server error"}))))))) :body "internal server error"})))))))
(defn- create-router (defn- create-router
[{:keys [session rpc google-auth gitlab-auth github-auth metrics ldap-auth svgparse assets] :as cfg}] [{:keys [session rpc oauth metrics svgparse assets] :as cfg}]
(rr/router (rr/router
[["/metrics" {:get (:handler metrics)}] [["/metrics" {:get (:handler metrics)}]
@ -140,16 +138,14 @@
["/svg" {:post svgparse}] ["/svg" {:post svgparse}]
["/oauth" ["/oauth"
["/google" {:post (:auth-handler google-auth)}] ["/google" {:post (get-in oauth [:google :handler])}]
["/google/callback" {:get (:callback-handler google-auth)}] ["/google/callback" {:get (get-in oauth [:google :callback-handler])}]
["/gitlab" {:post (:auth-handler gitlab-auth)}] ["/gitlab" {:post (get-in oauth [:gitlab :handler])}]
["/gitlab/callback" {:get (:callback-handler gitlab-auth)}] ["/gitlab/callback" {:get (get-in oauth [:gitlab :callback-handler])}]
["/github" {:post (:auth-handler github-auth)}] ["/github" {:post (get-in oauth [:github :handler])}]
["/github/callback" {:get (:callback-handler github-auth)}]] ["/github/callback" {:get (get-in oauth [:github :callback-handler])}]]
["/login-ldap" {:post ldap-auth}]
["/rpc" {:middleware [(:middleware session)]} ["/rpc" {:middleware [(:middleware session)]}
["/query/:type" {:get (:query-handler rpc)}] ["/query/:type" {:get (:query-handler rpc)}]

View file

@ -1,127 +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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.http.auth.ldap
(:require
[app.common.exceptions :as ex]
[app.config :as cfg]
[clj-ldap.client :as client]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[clojure.string ]
[clojure.tools.logging :as log]
[integrant.core :as ig]))
(declare authenticate)
(declare create-connection)
(declare replace-several)
(s/def ::host ::cfg/ldap-auth-host)
(s/def ::port ::cfg/ldap-auth-port)
(s/def ::ssl ::cfg/ldap-auth-ssl)
(s/def ::starttls ::cfg/ldap-auth-starttls)
(s/def ::user-query ::cfg/ldap-auth-user-query)
(s/def ::base-dn ::cfg/ldap-auth-base-dn)
(s/def ::username-attribute ::cfg/ldap-auth-username-attribute)
(s/def ::email-attribute ::cfg/ldap-auth-email-attribute)
(s/def ::fullname-attribute ::cfg/ldap-auth-fullname-attribute)
(s/def ::avatar-attribute ::cfg/ldap-auth-avatar-attribute)
(s/def ::rpc map?)
(s/def ::session map?)
(defmethod ig/pre-init-spec :app.http.auth/ldap
[_]
(s/keys
:req-un [::rpc ::session]
:opt-un [::host
::port
::ssl
::starttls
::username-attribute
::base-dn
::username-attribute
::email-attribute
::fullname-attribute
::avatar-attribute]))
(defmethod ig/init-key :app.http.auth/ldap
[_ {:keys [session rpc] :as cfg}]
(let [conn (create-connection cfg)]
(with-meta
(fn [request]
(let [data (:body-params request)]
(when-some [info (authenticate (assoc cfg
:conn conn
:username (:email data)
:password (:password data)))]
(let [method-fn (get-in rpc [:methods :mutation :login-or-register])
profile (method-fn {:email (:email info)
:backend "ldap"
:fullname (:fullname info)})
sxf ((:create session) (:id profile))
rsp {:status 200 :body profile}]
(sxf request rsp)))))
{::conn conn})))
(defmethod ig/halt-key! ::client
[_ handler]
(let [{:keys [::conn]} (meta handler)]
(when (realized? conn)
(.close @conn))))
(defn- replace-several [s & {:as replacements}]
(reduce-kv clojure.string/replace s replacements))
(defn- create-connection
[cfg]
(let [params (merge {:host {:address (:host cfg)
:port (:port cfg)}}
(-> cfg
(select-keys [:ssl
:starttls
:ldap-bind-dn
:ldap-bind-password])
(set/rename-keys {:ssl :ssl?
:starttls :startTLS?
:ldap-bind-dn :bind-dn
:ldap-bind-password :password})))]
(delay
(try
(client/connect params)
(catch Exception e
(log/errorf e "Cannot connect to LDAP %s:%s"
(:host cfg) (:port cfg)))))))
(defn- authenticate
[{:keys [conn username password] :as cfg}]
(when-some [conn (some-> conn deref)]
(let [user-search-query (replace-several (:user-query cfg) "$username" username)
user-attributes (-> cfg
(select-keys [:username-attribute
:email-attribute
:fullname-attribute
:avatar-attribute])
vals)]
(when-some [user-entry (-> conn
(client/search (:base-dn cfg)
{:filter user-search-query
:sizelimit 1
:attributes user-attributes})
(first))]
(when-not (client/bind? conn (:dn user-entry) password)
(ex/raise :type :authentication
:code :wrong-credentials))
(set/rename-keys user-entry {(keyword (:avatar-attribute cfg)) :photo
(keyword (:fullname-attribute cfg)) :fullname
(keyword (:email-attribute cfg)) :email})))))

View file

@ -46,6 +46,11 @@
[err _] [err _]
{:status 401 :body (ex-data err)}) {:status 401 :body (ex-data err)})
(defmethod handle-exception :restriction
[err _]
{:status 400 :body (ex-data err)})
(defmethod handle-exception :validation (defmethod handle-exception :validation
[err req] [err req]
(let [header (get-in req [:headers "accept"]) (let [header (get-in req [:headers "accept"])

View file

@ -7,12 +7,12 @@
;; ;;
;; Copyright (c) 2020-2021 UXBOX Labs SL ;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.http.auth.github (ns app.http.oauth.github
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg] [app.config :as cfg]
[app.http.auth.google :as gg] [app.http.oauth.google :as gg]
[app.util.http :as http] [app.util.http :as http]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.data.json :as json] [clojure.data.json :as json]
@ -137,7 +137,7 @@
(s/def ::session map?) (s/def ::session map?)
(s/def ::tokens fn?) (s/def ::tokens fn?)
(defmethod ig/pre-init-spec :app.http.auth/github [_] (defmethod ig/pre-init-spec :app.http.oauth/github [_]
(s/keys :req-un [::public-uri (s/keys :req-un [::public-uri
::session ::session
::tokens] ::tokens]
@ -148,12 +148,12 @@
[_] [_]
(ex/raise :type :not-found)) (ex/raise :type :not-found))
(defmethod ig/init-key :app.http.auth/github (defmethod ig/init-key :app.http.oauth/github
[_ cfg] [_ cfg]
(if (and (:client-id cfg) (if (and (:client-id cfg)
(:client-secret cfg)) (:client-secret cfg))
{:auth-handler #(auth-handler cfg %) {:handler #(auth-handler cfg %)
:callback-handler #(callback-handler cfg %)} :callback-handler #(callback-handler cfg %)}
{:auth-handler default-handler {:handler default-handler
:callback-handler default-handler})) :callback-handler default-handler}))

View file

@ -7,12 +7,12 @@
;; ;;
;; Copyright (c) 2020 UXBOX Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.http.auth.gitlab (ns app.http.oauth.gitlab
(:require (:require
[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.http.auth.google :as gg] [app.http.oauth.google :as gg]
[app.util.http :as http] [app.util.http :as http]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.data.json :as json] [clojure.data.json :as json]
@ -140,7 +140,7 @@
(s/def ::session map?) (s/def ::session map?)
(s/def ::tokens fn?) (s/def ::tokens fn?)
(defmethod ig/pre-init-spec :app.http.auth/gitlab [_] (defmethod ig/pre-init-spec :app.http.oauth/gitlab [_]
(s/keys :req-un [::public-uri (s/keys :req-un [::public-uri
::session ::session
::tokens] ::tokens]
@ -148,8 +148,7 @@
::client-id ::client-id
::client-secret])) ::client-secret]))
(defmethod ig/prep-key :app.http.oauth/gitlab
(defmethod ig/prep-key :app.http.auth/gitlab
[_ cfg] [_ cfg]
(d/merge {:base-uri "https://gitlab.com"} (d/merge {:base-uri "https://gitlab.com"}
(d/without-nils cfg))) (d/without-nils cfg)))
@ -158,11 +157,11 @@
[_] [_]
(ex/raise :type :not-found)) (ex/raise :type :not-found))
(defmethod ig/init-key :app.http.auth/gitlab (defmethod ig/init-key :app.http.oauth/gitlab
[_ cfg] [_ cfg]
(if (and (:client-id cfg) (if (and (:client-id cfg)
(:client-secret cfg)) (:client-secret cfg))
{:auth-handler #(auth-handler cfg %) {:handler #(auth-handler cfg %)
:callback-handler #(callback-handler cfg %)} :callback-handler #(callback-handler cfg %)}
{:auth-handler default-handler {:handler default-handler
:callback-handler default-handler})) :callback-handler default-handler}))

View file

@ -7,7 +7,7 @@
;; ;;
;; Copyright (c) 2020-2021 UXBOX Labs SL ;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.http.auth.google (ns app.http.oauth.google
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
@ -50,7 +50,6 @@
(when (= 200 (:status res)) (when (= 200 (:status res))
(-> (json/read-str (:body res)) (-> (json/read-str (:body res))
(get "access_token")))) (get "access_token"))))
(catch Exception e (catch Exception e
(log/error e "unexpected error on get-access-token") (log/error e "unexpected error on get-access-token")
nil))) nil)))
@ -60,8 +59,10 @@
(try (try
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo" (let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
:headers {"Authorization" (str "Bearer " token)} :headers {"Authorization" (str "Bearer " token)}
:timeout 6000
:method :get} :method :get}
res (http/send! req)] res (http/send! req)]
(when (= 200 (:status res)) (when (= 200 (:status res))
(let [data (json/read-str (:body res))] (let [data (json/read-str (:body res))]
{:email (get data "email") {:email (get data "email")
@ -78,6 +79,8 @@
info (some->> (get-in request [:params :code]) info (some->> (get-in request [:params :code])
(get-access-token cfg) (get-access-token cfg)
(get-user-info cfg))] (get-user-info cfg))]
(when-not info (when-not info
(ex/raise :type :internal (ex/raise :type :internal
:code :unable-to-auth)) :code :unable-to-auth))
@ -158,7 +161,7 @@
(s/def ::session map?) (s/def ::session map?)
(s/def ::tokens fn?) (s/def ::tokens fn?)
(defmethod ig/pre-init-spec :app.http.auth/google [_] (defmethod ig/pre-init-spec :app.http.oauth/google [_]
(s/keys :req-un [::public-uri (s/keys :req-un [::public-uri
::session ::session
::tokens] ::tokens]
@ -169,11 +172,11 @@
[_] [_]
(ex/raise :type :not-found)) (ex/raise :type :not-found))
(defmethod ig/init-key :app.http.auth/google (defmethod ig/init-key :app.http.oauth/google
[_ cfg] [_ cfg]
(if (and (:client-id cfg) (if (and (:client-id cfg)
(:client-secret cfg)) (:client-secret cfg))
{:auth-handler #(auth-handler cfg %) {:handler #(auth-handler cfg %)
:callback-handler #(callback-handler cfg %)} :callback-handler #(callback-handler cfg %)}
{:auth-handler default-handler {:handler default-handler
:callback-handler default-handler})) :callback-handler default-handler}))

View file

@ -95,7 +95,7 @@
(= k :profile-id) (assoc acc k (uuid/uuid v)) (= k :profile-id) (assoc acc k (uuid/uuid v))
(str/blank? v) acc (str/blank? v) acc
:else (assoc acc k v))) :else (assoc acc k v)))
{} {:id (uuid/next)}
(:context event))) (:context event)))
(defn- parse-event (defn- parse-event

View file

@ -87,10 +87,7 @@
:tokens (ig/ref :app.tokens/tokens) :tokens (ig/ref :app.tokens/tokens)
:public-uri (:public-uri config) :public-uri (:public-uri config)
:metrics (ig/ref :app.metrics/metrics) :metrics (ig/ref :app.metrics/metrics)
:google-auth (ig/ref :app.http.auth/google) :oauth (ig/ref :app.http.oauth/all)
:gitlab-auth (ig/ref :app.http.auth/gitlab)
:github-auth (ig/ref :app.http.auth/github)
:ldap-auth (ig/ref :app.http.auth/ldap)
:assets (ig/ref :app.http.assets/handlers) :assets (ig/ref :app.http.assets/handlers)
:svgparse (ig/ref :app.svgparse/handler) :svgparse (ig/ref :app.svgparse/handler)
:storage (ig/ref :app.storage/storage) :storage (ig/ref :app.storage/storage)
@ -104,7 +101,12 @@
:cache-max-age (dt/duration {:hours 24}) :cache-max-age (dt/duration {:hours 24})
:signature-max-age (dt/duration {:hours 24 :minutes 5})} :signature-max-age (dt/duration {:hours 24 :minutes 5})}
:app.http.auth/google :app.http.oauth/all
{:google (ig/ref :app.http.oauth/google)
:gitlab (ig/ref :app.http.oauth/gitlab)
:github (ig/ref :app.http.oauth/github)}
:app.http.oauth/google
{:rpc (ig/ref :app.rpc/rpc) {:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session) :session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens) :tokens (ig/ref :app.tokens/tokens)
@ -112,7 +114,7 @@
:client-id (:google-client-id config) :client-id (:google-client-id config)
:client-secret (:google-client-secret config)} :client-secret (:google-client-secret config)}
:app.http.auth/github :app.http.oauth/github
{:rpc (ig/ref :app.rpc/rpc) {:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session) :session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens) :tokens (ig/ref :app.tokens/tokens)
@ -120,7 +122,7 @@
:client-id (:github-client-id config) :client-id (:github-client-id config)
:client-secret (:github-client-secret config)} :client-secret (:github-client-secret config)}
:app.http.auth/gitlab :app.http.oauth/gitlab
{:rpc (ig/ref :app.rpc/rpc) {:rpc (ig/ref :app.rpc/rpc)
:session (ig/ref :app.http.session/session) :session (ig/ref :app.http.session/session)
:tokens (ig/ref :app.tokens/tokens) :tokens (ig/ref :app.tokens/tokens)
@ -129,20 +131,6 @@
:client-id (:gitlab-client-id config) :client-id (:gitlab-client-id config)
:client-secret (:gitlab-client-secret config)} :client-secret (:gitlab-client-secret config)}
:app.http.auth/ldap
{:host (:ldap-auth-host config)
:port (:ldap-auth-port config)
:ssl (:ldap-auth-ssl config)
:starttls (:ldap-auth-starttls config)
:user-query (:ldap-auth-user-query config)
:username-attribute (:ldap-auth-username-attribute config)
:email-attribute (:ldap-auth-email-attribute config)
:fullname-attribute (:ldap-auth-fullname-attribute config)
:avatar-attribute (:ldap-auth-avatar-attribute config)
:base-dn (:ldap-auth-base-dn config)
:session (ig/ref :app.http.session/session)
:rpc (ig/ref :app.rpc/rpc)}
:app.svgparse/svgc :app.svgparse/svgc
{:metrics (ig/ref :app.metrics/metrics)} {:metrics (ig/ref :app.metrics/metrics)}

View file

@ -127,6 +127,7 @@
'app.rpc.mutations.viewer 'app.rpc.mutations.viewer
'app.rpc.mutations.teams 'app.rpc.mutations.teams
'app.rpc.mutations.feedback 'app.rpc.mutations.feedback
'app.rpc.mutations.ldap
'app.rpc.mutations.verify-token) 'app.rpc.mutations.verify-token)
(map (partial process-method cfg)) (map (partial process-method cfg))
(into {})))) (into {}))))

View file

@ -265,7 +265,7 @@
(assoc params :file file))))) (assoc params :file file)))))
(defn- update-file (defn- update-file
[{:keys [conn] :as cfg} {:keys [file changes session-id] :as params}] [{:keys [conn] :as cfg} {:keys [file changes session-id profile-id] :as params}]
(when (> (:revn params) (when (> (:revn params)
(:revn file)) (:revn file))
(ex/raise :type :validation (ex/raise :type :validation
@ -287,6 +287,7 @@
(db/insert! conn :file-change (db/insert! conn :file-change
{:id (uuid/next) {:id (uuid/next)
:session-id session-id :session-id session-id
:profile-id profile-id
:file-id (:id file) :file-id (:id file)
:revn (:revn file) :revn (:revn file)
:data (:data file) :data (:data file)

View file

@ -0,0 +1,105 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020-2021 UXBOX Labs SL
(ns app.rpc.mutations.ldap
(:require
[app.common.exceptions :as ex]
[app.common.spec :as us]
[app.config :as cfg]
[app.rpc.mutations.profile :refer [login-or-register]]
[app.util.services :as sv]
[clj-ldap.client :as ldap]
[clojure.spec.alpha :as s]
[clojure.string]
[clojure.tools.logging :as log]))
(def cpool
(delay
(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
(log/errorf e "Cannot connect to LDAP %s:%s"
(get-in params [:host :address])
(get-in params [:host :port])))))))
;; --- Mutation: login-with-ldap
(declare authenticate)
(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 :rlimit :password}
[{:keys [pool session tokens] :as cfg} {:keys [email password invitation-token] :as params}]
(when-not @cpool
(ex/raise :type :restriction
:code :ldap-disabled
:hint "ldap disabled or unable to connect"))
(let [info (authenticate @cpool params)
cfg (assoc cfg :conn pool)]
(when-not info
(ex/raise :type :validation
:code :wrong-credentials))
(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))}))
(with-meta profile
{:transform-response ((:create session) (: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
[cpool {:keys [password] :as params}]
(when-let [{:keys [dn] :as luser} (get-ldap-user cpool params)]
(when (ldap/bind? cpool dn password)
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
:email (get luser (keyword (cfg/get :ldap-attrs-email)))
:backend "ldap"})))

View file

@ -204,10 +204,10 @@
(s/def ::login (s/def ::login
(s/keys :req-un [::email ::password] (s/keys :req-un [::email ::password]
:opt-un [::scope])) :opt-un [::scope ::invitation-token]))
(sv/defmethod ::login {:auth false :rlimit :password} (sv/defmethod ::login {:auth false :rlimit :password}
[{:keys [pool session] :as cfg} {:keys [email password scope] :as params}] [{:keys [pool session tokens] :as cfg} {:keys [email password scope] :as params}]
(letfn [(check-password [profile password] (letfn [(check-password [profile password]
(when (= (:password profile) "!") (when (= (:password profile) "!")
(ex/raise :type :validation (ex/raise :type :validation
@ -227,14 +227,27 @@
profile)] profile)]
(db/with-atomic [conn pool] (db/with-atomic [conn pool]
(let [prof (-> (profile/retrieve-profile-data-by-email conn email) (let [profile (-> (profile/retrieve-profile-data-by-email conn email)
(validate-profile) (validate-profile)
(profile/strip-private-attrs)) (profile/strip-private-attrs))
addt (profile/retrieve-additional-data conn (:id prof)) profile (merge profile (profile/retrieve-additional-data conn (:id profile)))]
prof (merge prof addt)] (if-let [token (:invitation-token params)]
(with-meta prof ;; If the request comes with an invitation token, this means
{:transform-response ((:create session) (:id prof))}))))) ;; that user wants to accept it with different user. A very
;; strange case but still can happen. In this case, we
;; proceed in the same way as in register: regenerate the
;; invitation token and return it to the user for proper
;; invitation acceptation.
(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))}))
(with-meta profile
{:transform-response ((:create session) (:id profile))}))))))
;; --- Mutation: Logout ;; --- Mutation: Logout
@ -249,12 +262,20 @@
;; --- Mutation: Register if not exists ;; --- Mutation: Register if not exists
(declare login-or-register)
(s/def ::backend ::us/string) (s/def ::backend ::us/string)
(s/def ::login-or-register (s/def ::login-or-register
(s/keys :req-un [::email ::fullname ::backend])) (s/keys :req-un [::email ::fullname ::backend]))
(sv/defmethod ::login-or-register {:auth false} (sv/defmethod ::login-or-register {:auth false}
[{:keys [pool] :as cfg} {:keys [email backend fullname] :as params}] [{:keys [pool] :as cfg} params]
(db/with-atomic [conn pool]
(-> (assoc cfg :conn conn)
(login-or-register params))))
(defn login-or-register
[{:keys [conn] :as cfg} {:keys [email backend] :as params}]
(letfn [(populate-additional-data [conn profile] (letfn [(populate-additional-data [conn profile]
(let [data (profile/retrieve-additional-data conn (:id profile))] (let [data (profile/retrieve-additional-data conn (:id profile))]
(merge profile data))) (merge profile data)))
@ -275,12 +296,11 @@
(create-profile-initial-data conn profile) (create-profile-initial-data conn profile)
profile))] profile))]
(db/with-atomic [conn pool]
(let [profile (profile/retrieve-profile-data-by-email conn email) (let [profile (profile/retrieve-profile-data-by-email conn email)
profile (if profile profile (if profile
(populate-additional-data conn profile) (populate-additional-data conn profile)
(register-profile conn params))] (register-profile conn params))]
(profile/strip-private-attrs profile))))) (profile/strip-private-attrs profile))))
;; --- Mutation: Update Profile (own) ;; --- Mutation: Update Profile (own)

View file

@ -303,7 +303,7 @@
team (db/get-by-id conn :team team-id) team (db/get-by-id conn :team team-id)
itoken (tokens :generate itoken (tokens :generate
{:iss :team-invitation {:iss :team-invitation
:exp (dt/in-future "24h") :exp (dt/in-future "6h")
:profile-id (:id profile) :profile-id (:id profile)
:role role :role role
:team-id team-id :team-id team-id

View file

@ -115,7 +115,7 @@
;; If the current session is already matches the invited ;; If the current session is already matches the invited
;; member, then just return the token and leave the frontend ;; member, then just return the token and leave the frontend
;; app redirect to correct team. ;; app redirect to correct team.
(assoc claims :status :created) (assoc claims :state :created)
;; If the session does not matches the invited member, replace ;; If the session does not matches the invited member, replace
;; the session with a new one matching the invited member. ;; the session with a new one matching the invited member.
@ -123,7 +123,7 @@
;; user clicking the link he already has access to the email ;; user clicking the link he already has access to the email
;; account. ;; account.
(with-meta (with-meta
(assoc claims :status :created) (assoc claims :state :created)
{:transform-response ((:create session) member-id)}))) {:transform-response ((:create session) member-id)})))
;; This happens when member-id is not filled in the invitation but ;; This happens when member-id is not filled in the invitation but

View file

@ -21,7 +21,7 @@
::spec sname ::spec sname
::name (name sname)) ::name (name sname))
sym (symbol (str "service-method-" (name sname)))] sym (symbol (str "sm$" (name sname)))]
`(do `(do
(def ~sym (fn ~args ~@body)) (def ~sym (fn ~args ~@body))
(reset-meta! (var ~sym) ~mdata)))) (reset-meta! (var ~sym) ~mdata))))

View file

@ -53,6 +53,7 @@ services:
- PENPOT_SMTP_PASSWORD= - PENPOT_SMTP_PASSWORD=
- PENPOT_SMTP_SSL=false - PENPOT_SMTP_SSL=false
- PENPOT_SMTP_TLS=false - PENPOT_SMTP_TLS=false
# LDAP setup # LDAP setup
- PENPOT_LDAP_HOST=ldap - PENPOT_LDAP_HOST=ldap
- PENPOT_LDAP_PORT=10389 - PENPOT_LDAP_PORT=10389
@ -61,10 +62,10 @@ services:
- PENPOT_LDAP_BASE_DN=ou=people,dc=planetexpress,dc=com - PENPOT_LDAP_BASE_DN=ou=people,dc=planetexpress,dc=com
- PENPOT_LDAP_BIND_DN=cn=admin,dc=planetexpress,dc=com - PENPOT_LDAP_BIND_DN=cn=admin,dc=planetexpress,dc=com
- PENPOT_LDAP_BIND_PASSWORD=GoodNewsEveryone - PENPOT_LDAP_BIND_PASSWORD=GoodNewsEveryone
- PENPOT_LDAP_USERNAME_ATTRIBUTE=uid - PENPOT_LDAP_ATTRS_USERNAME=uid
- PENPOT_LDAP_EMAIL_ATTRIBUTE=mail - PENPOT_LDAP_ATTRS_EMAIL=mail
- PENPOT_LDAP_FULLNAME_ATTRIBUTE=displayName - PENPOT_LDAP_ATTRS_FULLNAME=cn
- PENPOT_LDAP_AVATAR_ATTRIBUTE=jpegPhoto - PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
postgres: postgres:
image: postgres:13 image: postgres:13

View file

@ -773,7 +773,13 @@
"es" : "Actualizado: %s" "es" : "Actualizado: %s"
} }
}, },
"errors.auth.unauthorized" : { "errors.ldap-disabled" : {
"translations" : {
"en" : "LDAP authentication is disabled.",
"es" : "La autheticacion via LDAP esta deshabilitada."
}
},
"errors.wrong-credentials" : {
"used-in" : [ "src/app/main/ui/auth/login.cljs" ], "used-in" : [ "src/app/main/ui/auth/login.cljs" ],
"translations" : { "translations" : {
"en" : "Username or password seems to be wrong.", "en" : "Username or password seems to be wrong.",

View file

@ -1160,7 +1160,10 @@ input[type=range]:focus::-ms-fill-upper {
.icon { .icon {
padding: $small; padding: $small;
width: 40px; width: 48px;
height: 48px;
justify-content: center;
align-items: center;
} }
.content { .content {
@ -1169,6 +1172,7 @@ input[type=range]:focus::-ms-fill-upper {
font-size: $fs14; font-size: $fs14;
padding: $small; padding: $small;
width: 100%; width: 100%;
align-items: center;
} }
} }
@ -1227,7 +1231,6 @@ input[type=range]:focus::-ms-fill-upper {
&.inline { &.inline {
width: 100%; width: 100%;
margin-bottom: $big;
} }
} }

View file

@ -79,30 +79,6 @@
(watch [this state s] (watch [this state s]
(rx/of (logged-in profile))))) (rx/of (logged-in profile)))))
(defn login-with-ldap
[{:keys [email password] :as data}]
(us/verify ::login-params data)
(ptk/reify ::login-with-ldap
ptk/UpdateEvent
(update [_ state]
(merge state (dissoc initial-state :route :router)))
ptk/WatchEvent
(watch [this state s]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)
params {:email email
:password password
:scope "webapp"}]
(->> (rx/timer 100)
(rx/mapcat #(rp/mutation :login-with-ldap params))
(rx/tap on-success)
(rx/catch (fn [err]
(on-error err)
(rx/empty)))
(rx/map logged-in))))))
;; --- Logout ;; --- Logout
(def clear-user-data (def clear-user-data
@ -131,10 +107,11 @@
;; --- Register ;; --- Register
(s/def ::invitation-token ::us/not-empty-string)
(s/def ::register (s/def ::register
(s/keys :req-un [::fullname (s/keys :req-un [::fullname ::password ::email]
::password :opt-un [::invitation-token]))
::email]))
(defn register (defn register
"Create a register event instance." "Create a register event instance."

View file

@ -122,11 +122,5 @@
(seq params)) (seq params))
(send-mutation! id form))) (send-mutation! id form)))
(defmethod mutation :login-with-ldap
[id params]
(let [uri (str cfg/public-uri "/api/login-ldap")]
(->> (http/send! {:method :post :uri uri :body params})
(rx/mapcat handle-response))))
(def client-error? http/client-error?) (def client-error? http/client-error?)
(def server-error? http/server-error?) (def server-error? http/server-error?)

View file

@ -49,7 +49,7 @@
[:& register-success-page {:params params}] [:& register-success-page {:params params}]
:auth-login :auth-login
[:& login-page {:locale locale :params params}] [:& login-page {:params params}]
:auth-recovery-request :auth-recovery-request
[:& recovery-request-page {:locale locale}] [:& recovery-request-page {:locale locale}]

View file

@ -55,21 +55,40 @@
(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)))))
(defn- login-with-ldap
[event params]
(dom/prevent-default event)
(dom/stop-propagation event)
(let [{:keys [on-error]} (meta params)]
(->> (rp/mutation! :login-with-ldap params)
(rx/subs (fn [profile]
(if-let [token (:invitation-token profile)]
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
(st/emit! (da/logged-in profile))))
(fn [{:keys [type code] :as error}]
(cond
(and (= type :restriction)
(= code :ldap-disabled))
(st/emit! (dm/error (tr "errors.ldap-disabled")))
(fn? on-error)
(on-error error)))))))
(mf/defc login-form (mf/defc login-form
[] [{:keys [params] :as props}]
(let [error? (mf/use-state false) (let [error (mf/use-state false)
form (fm/use-form :spec ::login-form form (fm/use-form :spec ::login-form
:inital {}) :inital {})
on-error on-error
(fn [form event] (fn [_]
(reset! error? true)) (reset! error (tr "errors.wrong-credentials")))
on-submit on-submit
(mf/use-callback (mf/use-callback
(mf/deps form) (mf/deps form)
(fn [event] (fn [event]
(reset! error? false) (reset! error nil)
(let [params (with-meta (:clean-data @form) (let [params (with-meta (:clean-data @form)
{:on-error on-error})] {:on-error on-error})]
(st/emit! (da/login params))))) (st/emit! (da/login params)))))
@ -78,17 +97,15 @@
(mf/use-callback (mf/use-callback
(mf/deps form) (mf/deps form)
(fn [event] (fn [event]
(reset! error? false) (let [params (merge (:clean-data @form) params)]
(let [params (with-meta (:clean-data @form) (login-with-ldap event (with-meta params {:on-error on-error})))))]
{:on-error on-error})]
(st/emit! (da/login-with-ldap params)))))]
[:* [:*
(when @error? (when-let [message @error]
[:& msgs/inline-banner [:& msgs/inline-banner
{:type :warning {:type :warning
:content (tr "errors.auth.unauthorized") :content message
:on-close #(reset! error? false)}]) :on-close #(reset! error nil)}])
[:& fm/form {:on-submit on-submit :form form} [:& fm/form {:on-submit on-submit :form form}
[:div.fields-row [:div.fields-row
@ -114,13 +131,13 @@
:on-click on-submit-ldap}])]])) :on-click on-submit-ldap}])]]))
(mf/defc login-page (mf/defc login-page
[] [{:keys [params] :as props}]
[:div.generic-form.login-form [:div.generic-form.login-form
[:div.form-container [:div.form-container
[:h1 (tr "auth.login-title")] [:h1 (tr "auth.login-title")]
[:div.subtitle (tr "auth.login-subtitle")] [:div.subtitle (tr "auth.login-subtitle")]
[:& login-form {}] [:& login-form {:params params}]
[:div.links [:div.links
[:div.link-entry [:div.link-entry
@ -130,25 +147,25 @@
[:div.link-entry [:div.link-entry
[:span (tr "auth.register") " "] [:span (tr "auth.register") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-register)) [:a {:on-click #(st/emit! (rt/nav :auth-register {} params))
:tab-index "6"} :tab-index "6"}
(tr "auth.register-submit")]]] (tr "auth.register-submit")]]]
(when cfg/google-client-id (when cfg/google-client-id
[:a.btn-ocean.btn-large.btn-google-auth [:a.btn-ocean.btn-large.btn-google-auth
{:on-click login-with-google} {:on-click #(login-with-google % params)}
"Login with Google"]) "Login with Google"])
(when cfg/gitlab-client-id (when cfg/gitlab-client-id
[:a.btn-ocean.btn-large.btn-gitlab-auth [:a.btn-ocean.btn-large.btn-gitlab-auth
{:on-click login-with-gitlab} {:on-click #(login-with-gitlab % params)}
[:img.logo [:img.logo
{:src "/images/icons/brand-gitlab.svg"}] {:src "/images/icons/brand-gitlab.svg"}]
(tr "auth.login-with-gitlab-submit")]) (tr "auth.login-with-gitlab-submit")])
(when cfg/github-client-id (when cfg/github-client-id
[:a.btn-ocean.btn-large.btn-github-auth [:a.btn-ocean.btn-large.btn-github-auth
{:on-click login-with-github} {:on-click #(login-with-github % params)}
[:img.logo [:img.logo
{:src "/images/icons/brand-github.svg"}] {:src "/images/icons/brand-github.svg"}]
(tr "auth.login-with-github-submit")]) (tr "auth.login-with-github-submit")])

View file

@ -43,13 +43,11 @@
(s/def ::fullname ::us/not-empty-string) (s/def ::fullname ::us/not-empty-string)
(s/def ::password ::us/not-empty-string) (s/def ::password ::us/not-empty-string)
(s/def ::email ::us/email) (s/def ::email ::us/email)
(s/def ::token ::us/not-empty-string) (s/def ::invitation-token ::us/not-empty-string)
(s/def ::register-form (s/def ::register-form
(s/keys :req-un [::password (s/keys :req-un [::password ::fullname ::email]
::fullname :opt-un [::invitation-token]))
::email]
:opt-un [::token]))
(mf/defc register-form (mf/defc register-form
[{:keys [params] :as props}] [{:keys [params] :as props}]
@ -145,7 +143,7 @@
[:div.links [:div.links
[:div.link-entry [:div.link-entry
[:span (tr "auth.already-have-account") " "] [:span (tr "auth.already-have-account") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-login)) [:a {:on-click #(st/emit! (rt/nav :auth-login {} params))
:tab-index "4"} :tab-index "4"}
(tr "auth.login-here")]] (tr "auth.login-here")]]

View file

@ -52,10 +52,9 @@
[tdata] [tdata]
(case (:state tdata) (case (:state tdata)
:created :created
(let [message (tr "auth.notifications.team-invitation-accepted")] (st/emit! (dm/success (tr "auth.notifications.team-invitation-accepted"))
(st/emit! (du/fetch-profile) (du/fetch-profile)
(rt/nav :dashboard-projects {:team-id (:team-id tdata)}) (rt/nav :dashboard-projects {:team-id (:team-id tdata)}))
(dm/success message)))
:pending :pending
(let [token (:invitation-token tdata)] (let [token (:invitation-token tdata)]