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