module Yesod.Auth.LdapNative
(
authLdap
, authLdapWithForm
, LdapAuthConf
, LdapAuthQuery (..)
, mkLdapConf
, mkGroupQuery
, setHost
, setPort
, setUserQuery
, setGroupQuery
, setDebug
, L.Host (..)
) where
import Yesod.Core
import Yesod.Auth
import Yesod.Form
import Control.Applicative ((<$>), (<*>))
import Control.Exception (SomeException, IOException, Handler (..), catches)
import Control.Monad.Trans.Class
import Control.Monad.Trans.Either
import Data.Text (Text)
import Data.List.NonEmpty (NonEmpty (..), (<|))
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Ldap.Client as L
import qualified Ldap.Client.Bind as L
import qualified Ldap.Client.Search as L
import Ldap.Client
(Ldap, Dn, Password (..), Filter (..), Mod, Search, Attr (..), AttrValue, Host, PortNumber, LdapError, SearchEntry (..))
pluginName :: Text
pluginName = "ldap"
loginRoute :: AuthRoute
loginRoute = PluginR pluginName ["login"]
data LdapAuthConf = LdapAuthConf
{ host :: L.Host
, port :: L.PortNumber
, bindDn :: L.Dn
, bindPw :: L.Password
, userQuery :: LdapAuthQuery
, groupQuery :: Maybe LdapAuthQuery
, debug :: Int
}
data LdapAuthQuery = LdapAuthQuery L.Dn (L.Mod L.Search) (Text -> L.Filter) [L.Attr]
mkLdapConf
:: Text
-> Text
-> Text
-> LdapAuthConf
mkLdapConf bindDn bindPw baseDn = LdapAuthConf
{ host = L.Secure "localhost"
, port = 636
, bindDn = L.Dn bindDn
, bindPw = L.Password (T.encodeUtf8 bindPw)
, userQuery = mkUserQuery baseDn
, groupQuery = Nothing
, debug = 0
}
mkUserQuery
:: Text
-> LdapAuthQuery
mkUserQuery baseDn = LdapAuthQuery (L.Dn baseDn) (L.scope L.WholeSubtree)
(\u -> L.And $
L.Attr "objectClass" := "posixAccount"
<| L.Attr "uid" L.:= T.encodeUtf8 u
:| []
) []
mkGroupQuery
:: Text
-> Text
-> Text
-> Text
-> LdapAuthQuery
mkGroupQuery baseDn groupAttr groupName memberAttr = LdapAuthQuery (L.Dn baseDn) (L.scope L.WholeSubtree)
(\u -> L.And $
L.Attr "objectClass" := "posixGroup"
<| L.Attr groupAttr := T.encodeUtf8 groupName
<| L.Attr memberAttr := T.encodeUtf8 u
:| []
) []
setHost :: Host -> LdapAuthConf -> LdapAuthConf
setHost host conf = conf { host = host }
setPort :: PortNumber -> LdapAuthConf -> LdapAuthConf
setPort port conf = conf { port = port }
setUserQuery :: LdapAuthQuery -> LdapAuthConf -> LdapAuthConf
setUserQuery q conf = conf { userQuery = q }
setGroupQuery :: Maybe LdapAuthQuery -> LdapAuthConf -> LdapAuthConf
setGroupQuery q conf = conf { groupQuery = q }
setDebug :: Int -> LdapAuthConf -> LdapAuthConf
setDebug level conf = conf { debug = level }
authLdap :: YesodAuth m => LdapAuthConf -> AuthPlugin m
authLdap conf = authLdapWithForm conf defaultForm
authLdapWithForm :: (Yesod m, YesodAuth m) => LdapAuthConf -> (Route m -> WidgetT m IO ()) -> AuthPlugin m
authLdapWithForm conf form =
AuthPlugin pluginName (dispatch conf) $ \tp -> form (tp loginRoute)
dispatch :: LdapAuthConf -> Text -> [Text] -> AuthHandler master TypedContent
dispatch conf "POST" ["login"] = dispatchLdap conf
dispatch _ _ _ = notFound
dispatchLdap :: (RenderMessage site FormMessage) => LdapAuthConf -> AuthHandler site TypedContent
dispatchLdap conf = do
tp <- getRouteToParent
(username, password) <- lift $ runInputPost $ (,)
<$> ireq textField "username"
<*> ireq textField "password"
eb <- liftIO $
ldapLogin conf username password `catches` [Handler ioHandler, Handler catchAll]
case eb of
Left err -> do
case debug conf > 0 of
True -> setMessage $ [shamlet|<div.alert.alertdanger>Sign in failure. Error: #{show err}|]
False -> setMessage $ [shamlet|<div.alert.alertdanger>Sign in failure. That is all we know right now. Try again later.|]
lift $ redirect $ tp LoginR
Right (SearchEntry _ attrs) -> do
let extra = map f attrs
lift $ setCredsRedirect $ Creds pluginName username extra
where
f (L.Attr k, x : _) = (k, T.decodeUtf8 x)
f (L.Attr k, _) = (k, "")
ioHandler :: IOException -> IO (Either LdapAuthError SearchEntry)
ioHandler e = return $ Left $ IOException e
catchAll :: SomeException -> IO (Either LdapAuthError SearchEntry)
catchAll _ = return $ Left UnexpectedException
data LdapAuthError =
ResponseError L.ResponseError
| LdapError L.LdapError
| ServiceBindError
| UserNotFoundError
| MultipleUsersError
| UserBindError
| GroupMembershipError
| IOException IOException
| UnexpectedException
deriving (Eq, Show)
ldapLogin :: LdapAuthConf -> Text -> Text -> IO (Either LdapAuthError SearchEntry)
ldapLogin conf user pw = do
res <- L.with (host conf) (port conf) $ \l ->
runEitherT $ do
esb <- lift $ L.bindEither l (bindDn conf) (bindPw conf)
case esb of
Right _ -> return ()
Left _ -> left ServiceBindError
eu <- lift $ query l (userQuery conf) user
se@(SearchEntry dn _) <- case eu of
Right (x : []) -> return x
Right [] -> left UserNotFoundError
Right _ -> left MultipleUsersError
Left err -> left $ ResponseError err
let mg = groupQuery conf
eg <- case mg of
Just g -> lift $ query l g user
Nothing -> return $ Right []
case eg of
Right [] -> case mg of
Just _ -> left GroupMembershipError
Nothing -> return ()
Right _ -> return ()
Left err -> left $ ResponseError err
eub <- lift $ L.bindEither l dn (Password (T.encodeUtf8 pw))
case eub of
Right _ -> return ()
Left _ -> left UserBindError
return se
case res of
Left err -> return $ Left $ LdapError err
Right x -> return x
query :: Ldap -> LdapAuthQuery -> Text -> IO (Either L.ResponseError [SearchEntry])
query l (LdapAuthQuery baseDn mods filter attrs) login =
L.searchEither l baseDn mods (filter login) attrs
defaultForm :: Yesod app => Route app -> WidgetT app IO ()
defaultForm loginR = [whamlet|
<form class="login-form" action="@{loginR}" method="post">
<h2>Sign in
<div.formgroup>
<label>Username
<input.formcontrol type="text" name="username" required>
<div.formgroup>
<label>Password
<input.formcontrol type="password" name="password" required>
<button.btn.btnprimary type="submit">Submit
|]