batchd-0.1.1.0: src/Batchd/Daemon/Auth.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Batchd.Daemon.Auth where
import Control.Monad
import Control.Monad.Reader
import Data.Maybe
import Data.Int
import qualified Data.Text as T
import qualified Data.Vault.Lazy as V
import Database.Persist
import Database.Persist.Sql as Sql hiding (Single)
import qualified Data.ByteString as B
import Network.HTTP.Types (status401, hAuthorization)
import Network.Wai
import qualified Network.Wai.Middleware.HttpAuth as HA
import qualified Web.Scotty.Trans as Scotty
import System.Log.Heavy (LoggingTState)
import Data.Text.Format.Heavy
import System.IO.Unsafe (unsafePerformIO)
import Batchd.Core.Common.Types
import Batchd.Common.Types
import Batchd.Common.Data
import Batchd.Daemon.Types
import Batchd.Daemon.Crypto
import Batchd.Daemon.Database
import Batchd.Core.Daemon.Logging
-- | WAI Request vault key for currently authenticated user name
usernameKey :: V.Key String
usernameKey = unsafePerformIO V.newKey
{-# NOINLINE usernameKey #-}
---------- Users manipulation --------------------
-- | Create user
createUserDb :: String -- ^ User name
-> String -- ^ Password
-> String -- ^ Static part of salt
-> DB (Key User)
createUserDb name password staticSalt = do
dynamicSalt <- liftIO randomSalt
let hash = calcHash password dynamicSalt staticSalt
let user = User name hash dynamicSalt
insert user
-- | Create superuser
createSuperUserDb :: String -- ^ User name
-> String -- ^ Password
-> String -- ^ Static part of salt
-> DB (Key User)
createSuperUserDb name password staticSalt = do
userKey <- createUserDb name password staticSalt
let perm = UserPermission name SuperUser Nothing Nothing Nothing
insert perm
return userKey
-- | Create user
createUser :: GlobalConfig
-> LoggingTState
-> String -- ^ User name
-> String -- ^ Password
-> IO Bool
createUser gcfg lts name password = do
pool <- getPool gcfg lts
let staticSalt = getAuthStaticSalt $ mcAuth $ dbcManager gcfg
res <- runDBIO gcfg pool lts (createUserDb name password staticSalt)
case res of
Left _ -> return False
Right _ -> return True
-- | Create superuser
createSuperUser :: GlobalConfig
-> LoggingTState
-> String -- ^ User name
-> String -- ^ Password
-> IO Bool
createSuperUser gcfg lts name password = do
pool <- getPool gcfg lts
let staticSalt = getAuthStaticSalt $ mcAuth $ dbcManager gcfg
res <- runDBIO gcfg pool lts (createSuperUserDb name password staticSalt)
case res of
Left _ -> return False
Right _ -> return True
-- | Create permission
createPermission :: String -- ^ User name
-> UserPermission
-> DB (Key UserPermission)
createPermission name perm = do
let perm' = perm {userPermissionUserName = name}
insert perm'
-- | Get list of permissions and their IDs for specified user
getPermissions :: String -- ^ User name
-> DB [(Int64, UserPermission)]
getPermissions name = do
res <- selectList [UserPermissionUserName ==. name] []
let getKey (UserPermissionKey (SqlBackendKey id)) = id
return [(getKey (entityKey e), entityVal e) | e <- res]
-- | Delete permission. Fail if specified permission does not belong to specified user.
deletePermission :: Int64 -- ^ Permission ID
-> String -- ^ User name
-> DB ()
deletePermission id name = do
let pid = UserPermissionKey (SqlBackendKey id)
mbPerm <- get pid
case mbPerm of
Nothing -> throwR $ UnknownError "Permission does not exist"
Just perm -> do
if userPermissionUserName perm == name
then delete pid
else throwR $ UnknownError "Permission does not belong to specified user"
-- | Change user password
changePasswordDb :: String -- ^ User name
-> String -- ^ New password
-> String -- ^ Static part of salt
-> DB ()
changePasswordDb name password staticSalt = do
dynamicSalt <- liftIO randomSalt
let hash = calcHash password dynamicSalt staticSalt
key = UserKey name
update key [UserPwdHash =. hash, UserSalt =. dynamicSalt]
changePassword :: GlobalConfig
-> LoggingTState
-> String -- ^ User name
-> String -- ^ Password
-> IO Bool
changePassword gcfg lts name password = do
pool <- getPool gcfg lts
let staticSalt = getAuthStaticSalt $ mcAuth $ dbcManager gcfg
res <- runDBIO gcfg pool lts (changePasswordDb name password staticSalt)
case res of
Left _ -> return False
Right _ -> return True
-- | Get list of all users
getUsers :: DB [String]
getUsers = do
res <- selectList [] []
return $ map (userName . entityVal) res
------------------ Check user -----------------------
checkUserDb :: String -> String -> String -> DB Bool
checkUserDb name password staticSalt = do
mbUser <- get (UserKey name)
case mbUser of
Nothing -> return False
Just user -> do
let hash = calcHash password (userSalt user) staticSalt
return $ hash == userPwdHash user
checkUserExistsDb :: String -> DB Bool
checkUserExistsDb name = do
mbUser <- get (UserKey name)
return $ isJust mbUser
checkUser :: GlobalConfig -> LoggingTState -> (B.ByteString -> B.ByteString -> IO Bool)
checkUser gcfg lts nameBstr passwordBstr = do
pool <- getPool gcfg lts
let name = bstrToString nameBstr
password = bstrToString passwordBstr
salt = getAuthStaticSalt $ mcAuth $ dbcManager gcfg
res <- runDBIO gcfg pool lts (checkUserDb name password salt)
case res of
Left _ -> return False
Right r -> return r
checkUserExists :: GlobalConfig -> LoggingTState -> B.ByteString -> IO Bool
checkUserExists gcfg lts nameBstr = do
pool <- getPool gcfg lts
let name = bstrToString nameBstr
res <- runDBIO gcfg pool lts (checkUserExistsDb name)
case res of
Left _ -> return False
Right r -> return r
------------------ Authentication ---------------------
anonymousUserName :: String
anonymousUserName = "anonymous"
anonymousUser :: User
anonymousUser = User anonymousUserName "" ""
extractBasicUser :: Request -> String
extractBasicUser req =
case (lookup hAuthorization $ requestHeaders req) >>= HA.extractBasicAuth of
Nothing -> anonymousUserName
Just (name,_) -> bstrToString name
-- | Returns true if this is OPTIONS request for @/@
isRootOptions :: Request -> Bool
isRootOptions rq = requestMethod rq == "OPTIONS" && pathInfo rq == []
-- | HTTP basic auth middleware
basicAuth :: GlobalConfig -> LoggingTState -> Middleware
basicAuth gcfg lts app req sendResponse =
let settings = "batchd" :: HA.AuthSettings
username = extractBasicUser req
-- put user name extracted from header to vault
req' = req {vault = V.insert usernameKey username $ vault req}
in if isRootOptions req
then app req sendResponse -- OPTIONS / is available without auth
else case getAuthUserRq req of
Nothing -> do
infoIO lts $(here) "Will try to authenticate user with basic auth: {}" (Single username)
HA.basicAuth (checkUser gcfg lts) settings app req' sendResponse
Just _ -> app req sendResponse
-- | Authentication by X-Auth-User HTTP header
headerAuth :: GlobalConfig -> LoggingTState -> Middleware
headerAuth gcfg lts app req sendResponse = do
-- liftIO $ putStrLn $ "X-Auth-User: " ++ show req
if isRootOptions req
then app req sendResponse -- OPTIONS / is available without auth
else case lookup "X-Auth-User" (requestHeaders req) of
Nothing -> do
debugIO lts $(here) "No X-Auth-User header" ()
app req sendResponse
-- case getAuthUserRq req of
-- Nothing -> sendResponse $ responseLBS status401 [] "User name not provided"
-- Just _ -> app req sendResponse
Just name -> do
let username = bstrToString name
-- put user name extracted from header to vault
req' = req {vault = V.insert usernameKey username $ vault req}
ok <- liftIO $ checkUserExists gcfg lts name
infoIO lts $(here) "User from X-AUth-User header authenticated: {}" (Single username)
if ok
then app req' sendResponse
else sendResponse $ responseLBS status401 [] "Specified user does not exist"
-- | Unconditional authentication
noAuth :: GlobalConfig -> LoggingTState -> Middleware
noAuth gcfg lts app req sendResponse = do
let username = case lookup "X-Auth-User" (requestHeaders req) of
Just n -> bstrToString n
Nothing -> extractBasicUser req
-- if by some condition username appeared in header, put it to vault.
-- otherwise user name will be anonymous.
req' = req {vault = V.insert usernameKey username $ vault req}
infoIO lts $(here) "Authentication is disabled. User treated as superuser: {}" (Single username)
app req' sendResponse
-- authentication :: GlobalConfig -> Middleware
-- authentication gcfg =
-- let m1 = if dbcEnableHeaderAuth gcfg
-- then headerAuth gcfg
-- else id
-- m2 = if dbcEnableBasicAuth gcfg
-- then basicAuth gcfg
-- else id
-- in m2 . m1
------------ Get current user ------------
-- | Get user name from WAI Request vault
getAuthUserRq :: Request -> Maybe String
getAuthUserRq req = V.lookup usernameKey $ vault req
-- | Get currently authenticated user name.
-- Nothing means user was not authenticated.
getAuthUserName :: Action (Maybe String)
getAuthUserName = do
rq <- Scotty.request
return $ getAuthUserRq rq
-- | Get currently authenticated user.
-- Fail if user was not authenticated.
getAuthUser :: Action User
getAuthUser = do
cfg <- askConfigA
if isAuthDisabled $ mcAuth $ dbcManager cfg
then return anonymousUser
else do
mbName <- getAuthUserName
case mbName of
Nothing -> Scotty.raise $ InsufficientRights "user has to be authenticated"
Just name -> do
mbUser <- runDBA $ get (UserKey name)
case mbUser of
Nothing -> Scotty.raise $ UnknownError $ "user does not exist: " <> name
Just user -> return user
isAuthDisabled :: AuthMode -> Bool
isAuthDisabled AuthDisabled = True
isAuthDisabled _ = False
whenAuthEnabled :: Action () -> Action ()
whenAuthEnabled actions = do
cfg <- askConfigA
when (not $ isAuthDisabled $ mcAuth $ dbcManager cfg) $ actions
whenAuthEnabledCheck :: Action Bool -> Action Bool
whenAuthEnabledCheck check = do
cfg <- askConfigA
if isAuthDisabled $ mcAuth $ dbcManager cfg
then return True
else check
askAuthEnabled :: Action Bool
askAuthEnabled = do
cfg <- askConfigA
return (not $ isAuthDisabled $ mcAuth $ dbcManager cfg)
----------- Check user rights ---------------
-- | Check if user has SuperUser permission
isSuperUserDb :: String -> DB Bool
isSuperUserDb name = do
res <- selectList [UserPermissionUserName ==. name, UserPermissionPermission ==. SuperUser] []
return $ not $ null res
isSuperUser :: String -> Action Bool
isSuperUser name = do
whenAuthEnabledCheck $ runDBA $ isSuperUserDb name
-- | Check if user is superuser. Fail otherwise.
checkSuperUser :: Action ()
checkSuperUser = do
whenAuthEnabled $ do
user <- getAuthUser
ok <- runDBA $ isSuperUserDb (userName user)
when (not ok) $ do
Scotty.raise $ InsufficientRights "user has to be superuser"
-- | Check that user is authenticated. Fail otherwise.
checkUserAuthenticated :: Action ()
checkUserAuthenticated = do
_ <- getAuthUser
return ()
-- | Check if user has specified permission
hasPermissionDb :: String -- ^ User name
-> Permission
-> String -- ^ Queue name
-> DB Bool
hasPermissionDb name perm qname = do
super <- isSuperUserDb name
if super
then return True
else do
exact <- selectList [UserPermissionUserName ==. name, UserPermissionPermission ==. perm, UserPermissionQueueName ==. Just qname] []
if not (null exact)
then return True
else do
any <- selectList [UserPermissionUserName ==. name, UserPermissionPermission ==. perm, UserPermissionQueueName ==. Nothing] []
return $ not $ null any
-- | Check if user has permissions to create jobs in certain queue
hasCreatePermissionDb :: String -- ^ User name
-> String -- ^ Queue name
-> Maybe String -- ^ Just type name, or Nothing if you want to check that user has permission
-- to create jobs of at least some type
-> Maybe T.Text -- ^ Just host name, or Nothing if you want to check that user has permission
-- to create jobs on at least some host. Use special @__default__@ value
-- for default host of queue.
-> DB Bool
hasCreatePermissionDb name qname mbTypename mbHostname = do
super <- isSuperUserDb name
if super
then return True
else do
let byUser = [UserPermissionUserName ==. name, UserPermissionPermission ==. CreateJobs]
byQueue = [UserPermissionQueueName ==. Nothing] ||. [UserPermissionQueueName ==. Just qname]
byType = case mbTypename of
Nothing -> []
Just typename -> [UserPermissionTypeName ==. Nothing] ||. [UserPermissionTypeName ==. Just typename]
byHost = case mbHostname of
Nothing -> []
Just hostname -> [UserPermissionHostName ==. Nothing] ||. [UserPermissionHostName ==. Just hostname]
let filtr = byUser ++ byQueue ++ byType ++ byHost
res <- selectList filtr []
return $ not $ null res
-- | Check if user has permissions to create jobs in certain queue
hasCreatePermission :: String -- ^ User name
-> String -- ^ Queue name
-> Maybe String -- ^ Just type name, or Nothing if you want to check that user has permission
-- to create jobs of at least some type
-> Maybe T.Text -- ^ Just host name, or Nothing if you want to check that user has permission
-- to create jobs on at least some host. Use special @__default__@ value
-- for default host of queue.
-> Action Bool
hasCreatePermission name qname mbTypename mbHostname =
whenAuthEnabledCheck $ runDBA $ hasCreatePermissionDb name qname mbTypename mbHostname
-- | List names of hosts where user can create jobs of specified type in certain queue.
-- Returns Nothing if user can create jobs on any host.
listAllowedHostsDb :: String -- ^ User name
-> String -- ^ Queue name
-> String -- ^ Job type name
-> DB (Maybe [T.Text])
listAllowedHostsDb name qname typename = do
super <- isSuperUserDb name
if super
then return Nothing
else do
let byUser = [UserPermissionUserName ==. name, UserPermissionPermission ==. CreateJobs]
byQueue = [UserPermissionQueueName ==. Nothing] ||. [UserPermissionQueueName ==. Just qname]
byType = [UserPermissionTypeName ==. Nothing] ||. [UserPermissionTypeName ==. Just typename]
fltr = byUser ++ byQueue ++ byType
qryResult <- selectList fltr []
let mbHosts = map (userPermissionHostName . entityVal) qryResult
result = if any isNothing mbHosts -- in this case user can create jobs on any host
then Nothing
else Just $ map fromJust mbHosts
return result
-- | List names of hosts where user can create jobs of specified type in certain queue.
-- Returns Nothing if user can create jobs on any host.
listAllowedHosts :: String -- ^ User name
-> String -- ^ Queue name
-> String -- ^ Job type name
-> Action (Maybe [T.Text])
listAllowedHosts name qname typename = do
auth <- askAuthEnabled
if auth
then runDBA $ listAllowedHostsDb name qname typename
else return Nothing
-- | Check if user has permission to the full list of objects.
hasPermissionToListDb :: String -- ^ User name
-> Permission
-> DB Bool
hasPermissionToListDb name perm = do
super <- isSuperUserDb name
if super
then return True
else do
any <- selectList [UserPermissionUserName ==. name, UserPermissionPermission ==. perm, UserPermissionQueueName ==. Nothing] []
return $ not $ null any
-- | Check that user has specified permission. Fail otherwise.
checkPermission :: String -- ^ Error message for case of insufficient privileges
-> Permission
-> String -- ^ Queue name
-> Action ()
checkPermission message perm qname = do
whenAuthEnabled $ do
user <- getAuthUser
ok <- runDBA $ hasPermissionDb (userName user) perm qname
when (not ok) $ do
Scotty.raise $ InsufficientRights message
-- | Check if user can create jobs in given conditions. Fail otherwise.
checkCanCreateJobs :: String -- ^ Queue name
-> String -- ^ Job type name
-> T.Text -- ^ Host name. Use @__default__@ for default host of queue.
-> Action ()
checkCanCreateJobs qname typename hostname = do
whenAuthEnabled $ do
user <- getAuthUser
ok <- runDBA $ hasCreatePermissionDb (userName user) qname (Just typename) (Just hostname)
when (not ok) $ do
Scotty.raise $ InsufficientRights "create jobs"
-- | Check that user has permission to full list of objects. Fail otherwise.
checkPermissionToList :: String -- ^ Error message for case of insufficient privileges
-> Permission
-> Action ()
checkPermissionToList message perm = do
whenAuthEnabled $ do
user <- getAuthUser
ok <- runDBA $ hasPermissionToListDb (userName user) perm
when (not ok) $ do
Scotty.raise $ InsufficientRights message