diff --git a/backend/deps.edn b/backend/deps.edn index e544793a70..c396e0d4a8 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -53,6 +53,8 @@ com.draines/postal {:mvn/version "2.0.3" :exclusions [commons-codec/commons-codec]} + puppetlabs/clj-ldap {:mvn/version"0.3.0"} + ;; exception printing io.aviso/pretty {:mvn/version "0.1.37"} diff --git a/backend/src/uxbox/config.clj b/backend/src/uxbox/config.clj index 2299fb8d98..eac43c9246 100644 --- a/backend/src/uxbox/config.clj +++ b/backend/src/uxbox/config.clj @@ -44,7 +44,20 @@ :registration-enabled true :registration-domain-whitelist "" :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-debug ::us/boolean) @@ -78,6 +91,19 @@ (s/def ::google-client-id ::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/keys :opt-un [::http-server-cors ::http-server-debug @@ -106,7 +132,19 @@ ::allow-demo-users ::registration-enabled ::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 [env] diff --git a/backend/src/uxbox/http.clj b/backend/src/uxbox/http.clj index 5d9838c361..cc02c18d0a 100644 --- a/backend/src/uxbox/http.clj +++ b/backend/src/uxbox/http.clj @@ -19,6 +19,7 @@ [uxbox.http.handlers :as handlers] [uxbox.http.auth :as auth] [uxbox.http.auth.google :as google] + [uxbox.http.auth.ldap :as ldap] [uxbox.http.middleware :as middleware] [uxbox.http.session :as session] [uxbox.http.ws :as ws] @@ -48,6 +49,8 @@ :method :post}] ["/logout" {:handler auth/logout-handler :method :post}] + ["/login-ldap" {:handler ldap/auth + :method :post}] ["/w" {:middleware [session/auth]} ["/query/:type" {:get handlers/query-handler}] diff --git a/backend/src/uxbox/http/auth/ldap.clj b/backend/src/uxbox/http/auth/ldap.clj new file mode 100644 index 0000000000..d869cb3f3a --- /dev/null +++ b/backend/src/uxbox/http/auth/ldap.clj @@ -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})))) diff --git a/docs/05-Management-Guide.md b/docs/05-Management-Guide.md index 57944dda37..f755736c6e 100644 --- a/docs/05-Management-Guide.md +++ b/docs/05-Management-Guide.md @@ -7,11 +7,13 @@ **Only available at build time!** - `-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`) ## 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 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_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 ## diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 1d4bc778d0..ec348345ec 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -113,6 +113,7 @@ function readConfig(data) { const demoWarn = process.env.UXBOX_DEMO_WARNING; const deployDate = process.env.UXBOX_DEPLOY_DATE; const deployCommit = process.env.UXBOX_DEPLOY_COMMIT; + const loginWithLDAP = process.env.UXBOX_LOGIN_WITH_LDAP; let cfg = { demoWarning: demoWarn === "true" @@ -130,6 +131,10 @@ function readConfig(data) { cfg.deployCommit = deployCommit; } + if (loginWithLDAP !== undefined) { + cfg.loginWithLDAP = loginWithLDAP; + } + Object.assign(cfg, data); return JSON.stringify(cfg); diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 00c7b3e43a..98ffa4f9c1 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -107,6 +107,15 @@ "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" : { "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:89" ], "translations" : { diff --git a/frontend/src/uxbox/config.cljs b/frontend/src/uxbox/config.cljs index dc0e706140..3fce9afb96 100644 --- a/frontend/src/uxbox/config.cljs +++ b/frontend/src/uxbox/config.cljs @@ -15,10 +15,12 @@ puri (obj/get config "publicURI") wuri (obj/get config "workerURI") gcid (obj/get config "googleClientID" true) + lwl (obj/get config "loginWithLDAP" false) warn (obj/get config "demoWarning" true)] (def default-language "en") (def demo-warning warn) (def google-client-id gcid) + (def login-with-ldap lwl) (def worker-uri wuri) (def public-uri puri) (def default-theme "default"))) diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index 3047a3d815..186c130e75 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -78,6 +78,30 @@ (rx/of (du/profile-fetched profile) (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 (def clear-user-data diff --git a/frontend/src/uxbox/main/repo.cljs b/frontend/src/uxbox/main/repo.cljs index 5574fb4fab..927da0d789 100644 --- a/frontend/src/uxbox/main/repo.cljs +++ b/frontend/src/uxbox/main/repo.cljs @@ -104,5 +104,11 @@ (->> (http/send! {:method :post :uri uri :body params}) (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 server-error? http/server-error?) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index 16f69ac1b2..24ef2a38e8 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -43,6 +43,7 @@ (mf/defc login-form [{:keys [locale] :as props}] (let [error? (mf/use-state false) + submit-event (mf/use-var da/login) on-error (fn [form event] @@ -53,7 +54,7 @@ (reset! error? false) (let [params (with-meta (:clean-data form) {:on-error on-error})] - (st/emit! (da/login params))))] + (st/emit! (@submit-event params))))] [:* (when @error? @@ -78,7 +79,12 @@ :help-icon i/eye :label (t locale "auth.password-label")}] [:& 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 [{:keys [locale] :as props}] diff --git a/frontend/src/uxbox/main/ui/components/forms.cljs b/frontend/src/uxbox/main/ui/components/forms.cljs index 81a0e43b52..e5341bf318 100644 --- a/frontend/src/uxbox/main/ui/components/forms.cljs +++ b/frontend/src/uxbox/main/ui/components/forms.cljs @@ -121,12 +121,13 @@ i/arrow-slide]]])) (mf/defc submit-button - [{:keys [label form] :as props}] + [{:keys [label form on-click] :as props}] (let [form (mf/use-ctx form-ctx)] [:input.btn-primary.btn-large {:name "submit" :class (when-not (:valid form) "btn-disabled") :disabled (not (:valid form)) + :on-click on-click :value label :type "submit"}]))