(ns metabase.sso.ldap (:require [clj-ldap.client :as ldap] [metabase.models.setting :as setting] [metabase.sso.ldap.default-implementation :as default-impl] [metabase.sso.settings :as sso.settings] [metabase.util :as u] [metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.malli.schema :as ms]) (:import (com.unboundid.ldap.sdk LDAPConnectionPool LDAPException))) | |
(set! *warn-on-reflection* true) | |
Mappings from Metabase setting names to keys to use for LDAP connections | (def mb-settings->ldap-details
{:ldap-host :host
:ldap-port :port
:ldap-bind-dn :bind-dn
:ldap-password :password
:ldap-security :security
:ldap-user-base :user-base
:ldap-user-filter :user-filter
:ldap-attribute-email :attribute-email
:ldap-attribute-firstname :attribute-firstname
:ldap-attribute-lastname :attribute-lastname
:ldap-group-sync :group-sync
:ldap-group-base :group-base}) |
(defn- details->ldap-options [{:keys [host port bind-dn password security]}]
(let [security (keyword security)
port (if (string? port)
(Integer/parseInt port)
port)]
;; Connecting via IPv6 requires us to use this form for :host, otherwise
;; clj-ldap will find the first : and treat it as an IPv4 and port number
{:host {:address host
:port port}
:bind-dn bind-dn
:password password
:ssl? (= security :ssl)
:startTLS? (= security :starttls)})) | |
(defn- settings->ldap-options []
(details->ldap-options {:host (sso.settings/ldap-host)
:port (sso.settings/ldap-port)
:bind-dn (sso.settings/ldap-bind-dn)
:password (sso.settings/ldap-password)
:security (sso.settings/ldap-security)})) | |
Connects to LDAP with the currently set settings and returns the connection. | (defn- get-connection
^LDAPConnectionPool
[]
(let [options (settings->ldap-options)]
(log/debugf "Opening LDAP connection with options %s" (u/pprint-to-str options))
(try
(ldap/connect options)
(catch LDAPException e
(log/errorf "Failed to obtain LDAP connection: %s" (.getMessage e))
(throw e))))) |
Impl for [[with-ldap-connection]] macro. | (defn do-with-ldap-connection
[f]
(with-open [conn (get-connection)]
(f conn))) |
Execute | (defmacro with-ldap-connection
[[connection-binding] & body]
`(do-with-ldap-connection (fn [~(vary-meta connection-binding assoc :tag `LDAPConnectionPool)]
~@body))) |
TODO -- the usage of | (def ^:private user-base-error {:status :ERROR, :message "User search base does not exist or is unreadable"})
(def ^:private group-base-error {:status :ERROR, :message "Group search base does not exist or is unreadable"}) |
Test the connection to an LDAP server to determine if we can find the search base. Takes in a dictionary of properties such as: {:host "localhost" :port 389 :bind-dn "cn=Directory Manager" :password "password" :security "none" :user-base "ou=Birds,dc=metabase,dc=com" :group-base "ou=Groups,dc=metabase,dc=com"} | (defn test-ldap-connection
[{:keys [user-base group-base], :as details}]
(try
(with-open [^LDAPConnectionPool conn (ldap/connect (details->ldap-options details))]
(or
(try
(when-not (ldap/get conn user-base)
user-base-error)
(catch Exception _e
user-base-error))
(when group-base
(try
(when-not (ldap/get conn group-base)
group-base-error)
(catch Exception _e
group-base-error)))
(log/debug "LDAP connection test successful")
{:status :SUCCESS}))
(catch LDAPException e
(log/debug "LDAP connection test failed: " (.getMessage e))
{:status :ERROR, :message (.getMessage e), :code (.getResultCode e)})
(catch Exception e
(log/debug "LDAP connection test failed: " (.getMessage e))
{:status :ERROR, :message (.getMessage e)}))) |
Tests the connection to an LDAP server using the currently set settings. | (defn test-current-ldap-details
[]
(let [settings (into {} (for [[k v] mb-settings->ldap-details]
[v (setting/get k)]))]
(test-ldap-connection settings))) |
Verifies if the supplied password is valid for the | (defn verify-password
([user-info password]
(with-ldap-connection [conn]
(verify-password conn user-info password)))
([conn user-info password]
(let [dn (if (string? user-info) user-info (:dn user-info))]
(ldap/bind? conn dn password)))) |
A map of all ldap settings | (defn ldap-settings
[]
{:first-name-attribute (sso.settings/ldap-attribute-firstname)
:last-name-attribute (sso.settings/ldap-attribute-lastname)
:email-attribute (sso.settings/ldap-attribute-email)
:sync-groups? (sso.settings/ldap-group-sync)
:user-base (sso.settings/ldap-user-base)
:user-filter (sso.settings/ldap-user-filter)
:group-base (sso.settings/ldap-group-base)
:group-mappings (sso.settings/ldap-group-mappings)}) |
(mu/defn find-user :- [:maybe default-impl/UserInfo]
"Get user information for the supplied username."
([username :- ms/NonBlankString]
(with-ldap-connection [conn]
(find-user conn username)))
([ldap-connection :- (ms/InstanceOfClass LDAPConnectionPool)
username :- ms/NonBlankString]
(default-impl/find-user ldap-connection username (ldap-settings)))) | |
(mu/defn fetch-or-create-user! :- (ms/InstanceOf :model/User) "Using the `user-info` (from [[find-user]]) get the corresponding Metabase user, creating it if necessary." [user-info :- default-impl/UserInfo] (default-impl/fetch-or-create-user! user-info (ldap-settings))) | |
Convert raw error message responses from our LDAP tests into our normal api error response structure. | (defn humanize-error-messages
[{:keys [status message]}]
(when (not= :SUCCESS status)
(log/warn "Problem connecting to LDAP server:" message)
(let [conn-error {:errors {:ldap-host "Wrong host or port"
:ldap-port "Wrong host or port"}}
security-error {:errors {:ldap-port "Wrong port or security setting"
:ldap-security "Wrong port or security setting"}}
bind-dn-error {:errors {:ldap-bind-dn "Wrong bind DN"}}
creds-error {:errors {:ldap-bind-dn "Wrong bind DN or password"
:ldap-password "Wrong bind DN or password"}}]
(condp re-matches message
#".*UnknownHostException.*"
conn-error
#".*ConnectException.*"
conn-error
#".*SocketException.*"
security-error
#".*SSLException.*"
security-error
#"^For input string.*"
{:errors {:ldap-host "Invalid hostname, do not add the 'ldap://' or 'ldaps://' prefix"}}
#".*password was incorrect.*"
{:errors {:ldap-password "Password was incorrect"}}
#"^Unable to bind as user.*"
bind-dn-error
#"^Unable to parse bind DN.*"
{:errors {:ldap-bind-dn "Invalid bind DN"}}
#".*AcceptSecurityContext error, data 525,.*"
bind-dn-error
#".*AcceptSecurityContext error, data 52e,.*"
creds-error
#".*AcceptSecurityContext error, data 532,.*"
{:errors {:ldap-password "Password is expired"}}
#".*AcceptSecurityContext error, data 533,.*"
{:errors {:ldap-bind-dn "Account is disabled"}}
#".*AcceptSecurityContext error, data 701,.*"
{:errors {:ldap-bind-dn "Account is expired"}}
#"^User search base does not exist .*"
{:errors {:ldap-user-base "User search base does not exist or is unreadable"}}
#"^Group search base does not exist .*"
{:errors {:ldap-group-base "Group search base does not exist or is unreadable"}}
;; everything else :(
#"(?s).*"
{:message message})))) |