Merge branch 'other/ldap' into develop

This commit is contained in:
Andrey Antukh 2020-08-10 10:52:45 +02:00
commit b0fa8c3bfc
12 changed files with 186 additions and 6 deletions

View file

@ -53,6 +53,8 @@
com.draines/postal {:mvn/version "2.0.3" com.draines/postal {:mvn/version "2.0.3"
:exclusions [commons-codec/commons-codec]} :exclusions [commons-codec/commons-codec]}
puppetlabs/clj-ldap {:mvn/version"0.3.0"}
;; exception printing ;; exception printing
io.aviso/pretty {:mvn/version "0.1.37"} io.aviso/pretty {:mvn/version "0.1.37"}

View file

@ -44,7 +44,20 @@
:registration-enabled true :registration-enabled true
:registration-domain-whitelist "" :registration-domain-whitelist ""
:debug-humanize-transit true :debug-humanize-transit true
})
;; LDAP auth disabled by default. Set ldap-auth-host to enable
;:ldap-auth-host "ldap.mysupercompany.com"
;:ldap-auth-port 389
;:ldap-bind-dn "cn=admin,dc=ldap,dc=mysupercompany,dc=com"
;:ldap-bind-password "verysecure"
;: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"})
(s/def ::http-server-port ::us/integer) (s/def ::http-server-port ::us/integer)
(s/def ::http-server-debug ::us/boolean) (s/def ::http-server-debug ::us/boolean)
@ -78,6 +91,19 @@
(s/def ::google-client-id ::us/string) (s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string) (s/def ::google-client-secret ::us/string)
(s/def ::ldap-auth-host ::us/string)
(s/def ::ldap-auth-port ::us/integer)
(s/def ::ldap-bind-dn ::us/string)
(s/def ::ldap-bind-password ::us/string)
(s/def ::ldap-auth-ssl ::us/boolean)
(s/def ::ldap-auth-starttls ::us/boolean)
(s/def ::ldap-auth-base-dn ::us/string)
(s/def ::ldap-auth-user-query ::us/string)
(s/def ::ldap-auth-username-attribute ::us/string)
(s/def ::ldap-auth-email-attribute ::us/string)
(s/def ::ldap-auth-fullname-attribute ::us/string)
(s/def ::ldap-auth-avatar-attribute ::us/string)
(s/def ::config (s/def ::config
(s/keys :opt-un [::http-server-cors (s/keys :opt-un [::http-server-cors
::http-server-debug ::http-server-debug
@ -106,7 +132,19 @@
::allow-demo-users ::allow-demo-users
::registration-enabled ::registration-enabled
::registration-domain-whitelist ::registration-domain-whitelist
::image-process-max-threads])) ::image-process-max-threads
::ldap-auth-host
::ldap-auth-port
::ldap-bind-dn
::ldap-bind-password
::ldap-auth-ssl
::ldap-auth-starttls
::ldap-auth-base-dn
::ldap-auth-user-query
::ldap-auth-username-attribute
::ldap-auth-email-attribute
::ldap-auth-fullname-attribute
::ldap-auth-avatar-attribute]))
(defn env->config (defn env->config
[env] [env]

View file

@ -19,6 +19,7 @@
[uxbox.http.handlers :as handlers] [uxbox.http.handlers :as handlers]
[uxbox.http.auth :as auth] [uxbox.http.auth :as auth]
[uxbox.http.auth.google :as google] [uxbox.http.auth.google :as google]
[uxbox.http.auth.ldap :as ldap]
[uxbox.http.middleware :as middleware] [uxbox.http.middleware :as middleware]
[uxbox.http.session :as session] [uxbox.http.session :as session]
[uxbox.http.ws :as ws] [uxbox.http.ws :as ws]
@ -48,6 +49,8 @@
:method :post}] :method :post}]
["/logout" {:handler auth/logout-handler ["/logout" {:handler auth/logout-handler
:method :post}] :method :post}]
["/login-ldap" {:handler ldap/auth
:method :post}]
["/w" {:middleware [session/auth]} ["/w" {:middleware [session/auth]}
["/query/:type" {:get handlers/query-handler}] ["/query/:type" {:get handlers/query-handler}]

View file

@ -0,0 +1,69 @@
(ns uxbox.http.auth.ldap
(:require
[clj-ldap.client :as client]
[clojure.set :as set]
[mount.core :refer [defstate]]
[uxbox.common.exceptions :as ex]
[uxbox.config :as cfg]
[uxbox.services.mutations :as sm]
[uxbox.http.session :as session]
[clojure.tools.logging :as log]))
(defn replace-several [s & {:as replacements}]
(reduce-kv clojure.string/replace s replacements))
(defstate *ldap-pool
:start (delay
(try
(client/connect (merge {:host {:address (:ldap-auth-host cfg/config)
:port (:ldap-auth-port cfg/config)}}
(-> cfg/config
(select-keys [:ldap-auth-ssl
:ldap-auth-starttls
:ldap-bind-dn
:ldap-bind-password])
(set/rename-keys {:ldap-auth-ssl :ssl?
:ldap-auth-starttls :startTLS?
:ldap-bind-dn :bind-dn
:ldap-bind-password :password}))))
(catch Exception e
(log/errorf e "Cannot connect to LDAP %s:%s"
(:ldap-auth-host cfg/config) (:ldap-auth-port cfg/config)))))
:stop (when (realized? *ldap-pool)
(some-> *ldap-pool deref (.close))))
(defn- auth-with-ldap [username password]
(when-some [conn (some-> *ldap-pool deref)]
(let [user-search-query (replace-several (:ldap-auth-user-query cfg/config)
"$username" username)
user-attributes (-> cfg/config
(select-keys [:ldap-auth-username-attribute
:ldap-auth-email-attribute
:ldap-auth-fullname-attribute
:ldap-auth-avatar-attribute])
vals)]
(when-some [user-entry (-> conn
(client/search (:ldap-auth-base-dn cfg/config)
{: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 (:ldap-auth-avatar-attribute cfg/config)) :photo
(keyword (:ldap-auth-fullname-attribute cfg/config)) :fullname
(keyword (:ldap-auth-email-attribute cfg/config)) :email})))))
(defn auth [req]
(let [data (:body-params req)
uagent (get-in req [:headers "user-agent"])]
(when-some [info (auth-with-ldap (:email data) (:password data))]
(let [profile (sm/handle {::sm/type :login-or-register
:email (:email info)
:fullname (:fullname info)})
sid (session/create (:id profile) uagent)]
{:status 200
:cookies (session/cookies sid)
:body profile}))))

View file

@ -7,11 +7,13 @@
**Only available at build time!** **Only available at build time!**
- `-e UXBOX_PUBLIC_URI=...` (defaults to `http://localhost:6060`) - `-e UXBOX_PUBLIC_URI=...` (defaults to `http://localhost:6060`)
- `-e UXBOX_GOOGLE_CLIENT_ID=...` (defaults to `true`)
- `-e UXBOX_LOGIN_WITH_LDAP=...` (defaults to `false`)
- `-e UXBOX_DEMO_WARNING=...` (defaults to `true`) - `-e UXBOX_DEMO_WARNING=...` (defaults to `true`)
## Backend configuration parameters ## ## Backend configuration parameters ##
Backend accepts a bunch of configuration parameters (detailed abowe), Backend accepts a bunch of configuration parameters (detailed above),
that can be passed in different ways. The preferred one is using that can be passed in different ways. The preferred one is using
environment variables. environment variables.
@ -41,6 +43,19 @@ respective defaults):
- `UXBOX_REGISTRATION_DOMAIN_WHITELIST=""` (comma-separated domains, defaults to `""` which means that all domains are allowed) - `UXBOX_REGISTRATION_DOMAIN_WHITELIST=""` (comma-separated domains, defaults to `""` which means that all domains are allowed)
- `UXBOX_DEBUG_HUMANIZE_TRANSIT=true` - `UXBOX_DEBUG_HUMANIZE_TRANSIT=true`
- `UXBOX_LDAP_AUTH_HOST=` (default undefined)
- `UXBOX_LDAP_AUTH_PORT=` (default undefined)
- `UXBOX_LDAP_AUTH_VERSION=3`
- `UXBOX_LDAP_BIND_DN=` (default undefined)
- `UXBOX_LDAP_BIND_PASSWORD=` (default undefined)
- `UXBOX_LDAP_AUTH_SSL=` (default `false`)
- `UXBOX_LDAP_AUTH_STARTTLS=` (default `false`)
- `UXBOX_LDAP_AUTH_BASE_DN=` (default undefined)
- `UXBOX_LDAP_AUTH_USER_QUERY=(|(uid=$username)(mail=$username))`
- `UXBOX_LDAP_AUTH_USERNAME_ATTRIBUTE=uid`
- `UXBOX_LDAP_AUTH_EMAIL_ATTRIBUTE=mail`
- `UXBOX_LDAP_AUTH_FULLNAME_ATTRIBUTE=displayName`
- `UXBOX_LDAP_AUTH_AVATAR_ATTRIBUTE=jpegPhoto`
## REPL ## ## REPL ##

View file

@ -113,6 +113,7 @@ function readConfig(data) {
const demoWarn = process.env.UXBOX_DEMO_WARNING; const demoWarn = process.env.UXBOX_DEMO_WARNING;
const deployDate = process.env.UXBOX_DEPLOY_DATE; const deployDate = process.env.UXBOX_DEPLOY_DATE;
const deployCommit = process.env.UXBOX_DEPLOY_COMMIT; const deployCommit = process.env.UXBOX_DEPLOY_COMMIT;
const loginWithLDAP = process.env.UXBOX_LOGIN_WITH_LDAP;
let cfg = { let cfg = {
demoWarning: demoWarn === "true" demoWarning: demoWarn === "true"
@ -130,6 +131,10 @@ function readConfig(data) {
cfg.deployCommit = deployCommit; cfg.deployCommit = deployCommit;
} }
if (loginWithLDAP !== undefined) {
cfg.loginWithLDAP = loginWithLDAP;
}
Object.assign(cfg, data); Object.assign(cfg, data);
return JSON.stringify(cfg); return JSON.stringify(cfg);

View file

@ -107,6 +107,15 @@
"es" : "Entrar" "es" : "Entrar"
} }
}, },
"auth.login-with-ldap-submit-label" : {
"used-in" : [ "src/uxbox/main/ui/auth/login.cljs:108" ],
"translations" : {
"en" : "Sign in with LDAP",
"fr" : "Se connecter via LDAP",
"es" : "Entrar con LDAP",
"ru" : "Вход через LDAP"
}
},
"auth.login-subtitle" : { "auth.login-subtitle" : {
"used-in" : [ "src/uxbox/main/ui/auth/login.cljs:89" ], "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:89" ],
"translations" : { "translations" : {

View file

@ -15,10 +15,12 @@
puri (obj/get config "publicURI") puri (obj/get config "publicURI")
wuri (obj/get config "workerURI") wuri (obj/get config "workerURI")
gcid (obj/get config "googleClientID" true) gcid (obj/get config "googleClientID" true)
lwl (obj/get config "loginWithLDAP" false)
warn (obj/get config "demoWarning" true)] warn (obj/get config "demoWarning" true)]
(def default-language "en") (def default-language "en")
(def demo-warning warn) (def demo-warning warn)
(def google-client-id gcid) (def google-client-id gcid)
(def login-with-ldap lwl)
(def worker-uri wuri) (def worker-uri wuri)
(def public-uri puri) (def public-uri puri)
(def default-theme "default"))) (def default-theme "default")))

View file

@ -78,6 +78,30 @@
(rx/of (du/profile-fetched profile) (rx/of (du/profile-fetched profile)
(rt/nav' :dashboard-team {:team-id team-id})))))) (rt/nav' :dashboard-team {:team-id team-id}))))))
(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

View file

@ -104,5 +104,11 @@
(->> (http/send! {:method :post :uri uri :body params}) (->> (http/send! {:method :post :uri uri :body params})
(rx/mapcat handle-response)))) (rx/mapcat handle-response))))
(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

@ -43,6 +43,7 @@
(mf/defc login-form (mf/defc login-form
[{:keys [locale] :as props}] [{:keys [locale] :as props}]
(let [error? (mf/use-state false) (let [error? (mf/use-state false)
submit-event (mf/use-var da/login)
on-error on-error
(fn [form event] (fn [form event]
@ -53,7 +54,7 @@
(reset! error? false) (reset! error? false)
(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! (@submit-event params))))]
[:* [:*
(when @error? (when @error?
@ -78,7 +79,12 @@
:help-icon i/eye :help-icon i/eye
:label (t locale "auth.password-label")}] :label (t locale "auth.password-label")}]
[:& submit-button [:& submit-button
{:label (t locale "auth.login-submit-label")}]]])) {:label (t locale "auth.login-submit-label")
:on-click #(reset! submit-event da/login)}]
(when cfg/login-with-ldap
[:& submit-button
{:label (t locale "auth.login-with-ldap-submit-label")
:on-click #(reset! submit-event da/login-with-ldap)}])]]))
(mf/defc login-page (mf/defc login-page
[{:keys [locale] :as props}] [{:keys [locale] :as props}]

View file

@ -121,12 +121,13 @@
i/arrow-slide]]])) i/arrow-slide]]]))
(mf/defc submit-button (mf/defc submit-button
[{:keys [label form] :as props}] [{:keys [label form on-click] :as props}]
(let [form (mf/use-ctx form-ctx)] (let [form (mf/use-ctx form-ctx)]
[:input.btn-primary.btn-large [:input.btn-primary.btn-large
{:name "submit" {:name "submit"
:class (when-not (:valid form) "btn-disabled") :class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form)) :disabled (not (:valid form))
:on-click on-click
:value label :value label
:type "submit"}])) :type "submit"}]))