From 07029edc338cf242bfd41ab97c486cecc74ee6cf Mon Sep 17 00:00:00 2001
From: William Turner <william.turner@aero.bombardier.com>
Date: Wed, 8 Feb 2017 17:54:03 -0500
Subject: [PATCH] Adds basic working auth endpoint

---
 .../src/metabase/admin/settings/selectors.js  | 71 ++++++++++++++
 project.clj                                   |  3 +-
 .../050_add_user_ldap_auth_column.yaml        | 14 +++
 src/metabase/api/session.clj                  | 95 ++++++++++++++++++-
 src/metabase/api/user.clj                     |  4 +-
 src/metabase/models/user.clj                  | 12 +++
 6 files changed, 195 insertions(+), 4 deletions(-)
 create mode 100644 resources/migrations/050_add_user_ldap_auth_column.yaml

diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index 1303d13df83..e0312348473 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -156,6 +156,77 @@ const SECTIONS = [
             }
         ]
     },
+    {
+        name: "LDAP",
+        settings: [
+            {
+                key: "ldap-host",
+                display_name: "LDAP Host",
+                placeholder: "ldap.yourdomain.org",
+                type: "string",
+                required: true,
+                autoFocus: true
+            },
+            {
+                key: "ldap-port",
+                display_name: "LDAP Port",
+                placeholder: "389",
+                type: "string",
+                required: true,
+                validations: [["integer", "That's not a valid port number"]]
+            },
+            {
+                key: "ldap-security",
+                display_name: "LDAP Security",
+                description: null,
+                type: "radio",
+                options: { none: "None", ssl: "SSL", tls: "TLS" },
+                defaultValue: 'none'
+            },
+            {
+                key: "ldap-bind-dn",
+                display_name: "Username or DN",
+                type: "string",
+                required: true
+            },
+            {
+                key: "ldap-password",
+                display_name: "Password",
+                type: "password",
+                required: true
+            },
+            {
+                key: "ldap-base",
+                display_name: "Search base",
+                type: "string",
+                required: true
+            },
+            {
+                key: "ldap-user-filter",
+                display_name: "User filter",
+                type: "string",
+                required: true
+            },
+            {
+                key: "ldap-attribute-email",
+                display_name: "User email attribute",
+                type: "string",
+                required: true
+            },
+            {
+                key: "ldap-attribute-firstname",
+                display_name: "User first name attribute",
+                type: "string",
+                required: true
+            },
+            {
+                key: "ldap-attribute-lastname",
+                display_name: "User last name attribute",
+                type: "string",
+                required: true
+            }
+        ]
+    },
     {
         name: "Maps",
         settings: [
diff --git a/project.clj b/project.clj
index f972daf60dc..c0a154cfb07 100644
--- a/project.clj
+++ b/project.clj
@@ -78,7 +78,8 @@
                  [ring/ring-json "0.4.0"]                             ; Ring middleware for reading/writing JSON automatically
                  [stencil "0.5.0"]                                    ; Mustache templates for Clojure
                  [toucan "1.0.2"                                      ; Model layer, hydration, and DB utilities
-                  :exclusions [honeysql]]]
+                  :exclusions [honeysql]]
+                 [org.clojars.pntblnk/clj-ldap "0.0.12"]]             ; LDAP library
   :repositories [["bintray" "https://dl.bintray.com/crate/crate"]]    ; Repo for Crate JDBC driver
   :plugins [[lein-environ "1.1.0"]                                    ; easy access to environment variables
             [lein-ring "0.11.0"                                       ; start the HTTP server with 'lein ring server'
diff --git a/resources/migrations/050_add_user_ldap_auth_column.yaml b/resources/migrations/050_add_user_ldap_auth_column.yaml
new file mode 100644
index 00000000000..3689b62e343
--- /dev/null
+++ b/resources/migrations/050_add_user_ldap_auth_column.yaml
@@ -0,0 +1,14 @@
+databaseChangeLog:
+  - changeSet:
+      id: 50
+      author: wwwiiilll
+      changes:
+        - addColumn:
+            tableName: core_user
+            columns:
+              - column:
+                  name: ldap_auth
+                  type: boolean
+                  defaultValueBoolean: false
+                  constraints:
+                    nullable: false
diff --git a/src/metabase/api/session.clj b/src/metabase/api/session.clj
index 7dd4503679b..93893241a7a 100644
--- a/src/metabase/api/session.clj
+++ b/src/metabase/api/session.clj
@@ -1,9 +1,11 @@
 (ns metabase.api.session
   "/api/session endpoints"
-  (:require [clojure.tools.logging :as log]
+  (:require [clojure.string :as str]
+            [clojure.tools.logging :as log]
             [cemerick.friend.credentials :as creds]
             [cheshire.core :as json]
             [clj-http.client :as http]
+            [clj-ldap.client :as ldap]
             [compojure.core :refer [defroutes GET POST DELETE]]
             [schema.core :as s]
             [throttle.core :as throttle]
@@ -13,7 +15,7 @@
             [metabase.events :as events]
             (metabase.models [user :refer [User], :as user]
                              [session :refer [Session]]
-                             [setting :refer [defsetting]])
+                             [setting :refer [defsetting], :as setting])
             [metabase.public-settings :as public-settings]
             [metabase.util :as u]
             (metabase.util [password :as pass]
@@ -194,4 +196,93 @@
     (google-auth-fetch-or-create-user! given_name family_name email)))
 
 
+;;; ------------------------------------------------------------ LDAP AUTH ------------------------------------------------------------
+
+(defsetting ldap-host
+  "Server hostname. If this is set, LDAP auth is considered to be enabled.")
+
+(defsetting ldap-port
+  "Server port."
+  :default "389")
+
+(defsetting ldap-security
+  "Connect over SSL (LDAPS) or TLS"
+  :default "none"
+  :setter  (fn [new-value]
+             (when-not (nil? new-value)
+               (assert (contains? #{"none" "ssl" "tls"} new-value)))
+             (setting/set-string! :ldap-security new-value)))
+
+(defsetting ldap-bind-dn
+  "The DN to bind as.")
+
+(defsetting ldap-password
+  "The password to bind with.")
+
+(defsetting ldap-base
+  "Search base for users."
+  :default "")
+
+(defsetting ldap-user-filter
+  "Filter to use for looking up a specific user, the placeholder {login} will be replaced by the user supplied value."
+  :default "(&(objectClass=inetOrgPerson)(|(uid={login})(mail={login})))")
+
+(defsetting ldap-attribute-email
+  "Attribute to use for the user's email."
+  :default "mail")
+
+(defsetting ldap-attribute-firstname
+  "Attribute to use for the user's first name."
+  :default "givenName")
+
+(defsetting ldap-attribute-lastname
+  "Attribute to use for the user's last name."
+  :default "sn")
+
+(defn ldap-configured?
+  "Predicate function which returns `true` if we have an LDAP server configured, `false` otherwise."
+  []
+  (boolean (ldap-host)))
+
+(defn- ldap-connection []
+  (ldap/connect {:host      (str (ldap-host) ":" (ldap-port))
+                 :bind-dn   (ldap-bind-dn)
+                 :password  (ldap-password)
+                 :ssl?      (= (ldap-security) "ssl")
+                 :startTLS? (= (ldap-security) "tls")}))
+
+(defn- ldap-auth-user-info [email password]
+  (let [fname-attr (keyword (ldap-attribute-firstname))
+        lname-attr (keyword (ldap-attribute-lastname))
+        email-attr (keyword (ldap-attribute-email))]
+    ;; first figure out the user info if it even exists
+    (when-let [[result] (ldap/search (ldap-connection) (ldap-base) {:scope      :sub
+                                                                    :filter     (str/replace (ldap-user-filter) "{login}" email)
+                                                                    :attributes [:dn :distinguishedName fname-attr lname-attr email-attr]
+                                                                    :size-limit 1})]
+      ;; then validate the password by binding with it
+      (when (ldap/bind? (ldap-connection) (or (:dn result) (:distinguishedName result)) password)
+        {:first-name (get result fname-attr)
+         :last-name  (get result lname-attr)
+         :email      (get result email-attr)}))))
+
+(defn- ldap-auth-fetch-or-create-user! [first-name last-name email]
+ (if-let [user (or (db/select-one [User :id :last_login] :email email)
+                   (user/create-new-ldap-auth-user! first-name last-name email))]
+   {:id (create-session! user)}))
+
+(defendpoint POST "/ldap_auth"
+  "Login with LDAP auth."
+  [:as {{:keys [email password]} :body, remote-address :remote-addr}]
+  {email    su/Email
+   password su/NonBlankString}
+  (throttle/check (login-throttlers :ip-address) remote-address)
+  (throttle/check (login-throttlers :email)      email)
+  (let [user (ldap-auth-user-info email password)]
+    (when (nil? user)
+      (throw (ex-info "Password did not match stored password." {:status-code 400
+                                                                 :errors      {:password "did not match stored password"}})))
+    (ldap-auth-fetch-or-create-user! (:first-name user) (:last-name user) (:email user))))
+
+
 (define-routes)
diff --git a/src/metabase/api/user.clj b/src/metabase/api/user.clj
index 173de93aebe..3a43d045a2c 100644
--- a/src/metabase/api/user.clj
+++ b/src/metabase/api/user.clj
@@ -31,7 +31,9 @@
       :is_superuser  false
       ;; if the user orignally logged in via Google Auth and it's no longer enabled, convert them into a regular user (see Issue #3323)
       :google_auth   (boolean (and (:google_auth existing-user)
-                                   (session-api/google-auth-client-id))))) ; if google-auth-client-id is set it means Google Auth is enabled
+                                   (session-api/google-auth-client-id))) ; if google-auth-client-id is set it means Google Auth is enabled
+      :ldap_auth     (boolean (and (:ldap_auth existing-user)
+                                   (session-api/ldap-configured?)))))
   ;; now return the existing user whether they were originally active or not
   (User (u/get-id existing-user)))
 
diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj
index be7042e2c81..f18bddaa74a 100644
--- a/src/metabase/models/user.clj
+++ b/src/metabase/models/user.clj
@@ -141,6 +141,18 @@
     ;; send an email to everyone including the site admin if that's set
     (email/send-user-joined-admin-notification-email! <>, :google-auth? true)))
 
+(defn create-new-ldap-auth-user!
+  "Convenience for creating a new user via Google Auth. This account is considered active immediately; thus all active admins will recieve an email right away."
+  [first-name last-name email-address]
+  {:pre [(string? first-name) (string? last-name) (u/is-email? email-address)]}
+  (u/prog1 (db/insert! User
+             :email      email-address
+             :first_name first-name
+             :last_name  last-name
+             :password   (str (UUID/randomUUID))
+             :ldap_auth  true)
+    ;; send an email to everyone including the site admin if that's set
+    (email/send-user-joined-admin-notification-email! <>, :ldap-auth? true)))
 
 
 (defn set-password!
-- 
GitLab