{-# LANGUAGE DoAndIfThenElse #-} {-# LANGUAGE OverloadedStrings #-} module Web.Spock.Auth ( -- * Initialisation helpers authSessCfg, AuthCfg (..) -- * Handeling custom session data , writeSessionData, readSessionData, modifySessionData -- * Access control , VisitorSession, NoAccessReason (..) , NoAccessHandler, LoadUserFun, CheckRightsFun, UserRights , markAsLoggedIn , markAsGuest , userRoute ) where import Web.Spock import Control.Applicative import Data.Time.Clock import qualified Network.HTTP.Types as Http import qualified Data.Text as T -- | Configuration data AuthCfg sess = AuthCfg { ac_sessionTTL :: NominalDiffTime , ac_emptySession :: sess } -- | Assign the current session roles/permission, eg. admin or user type UserRights = T.Text -- | Describes why access was denied to a user data NoAccessReason = NotEnoughRights | NotLoggedIn | NotValidUser deriving (Show, Eq, Read, Enum) -- | Define what happens to non-authorized requests type NoAccessHandler conn sess userId st = NoAccessReason -> SpockAction conn (VisitorSession sess userId) st () -- | How should a session be transformed into a user? Can access the database using 'runQuery' type LoadUserFun conn sess userId st user = userId -> SpockAction conn (VisitorSession sess userId) st (Maybe user) -- | What rights does the current user have? Can access the database using 'runQuery' type CheckRightsFun conn sess userId st user = user -> [UserRights] -> SpockAction conn (VisitorSession sess userId) st Bool data SessionType userId = GuestSession | UserSession userId deriving (Show, Eq) data VisitorSession sess userId = VisitorSession { vs_type :: SessionType userId , vs_data :: sess } deriving (Show, Eq) -- | Plug this into the 'spock' function to create SessionCfg authSessCfg :: AuthCfg sess -> SessionCfg (VisitorSession sess userId) authSessCfg authCfg = SessionCfg { sc_cookieName = "spocksession" , sc_sessionTTL = ac_sessionTTL authCfg , sc_sessionIdEntropy = 42 , sc_emptySession = VisitorSession GuestSession (ac_emptySession authCfg) } -- | Mark current visitor as logged in markAsLoggedIn :: userId -> SpockAction conn (VisitorSession sess userId) st () markAsLoggedIn userId = modifySession (\oldData -> oldData { vs_type = (UserSession userId) }) -- | Mark current visitor as guest markAsGuest :: SpockAction conn (VisitorSession sess userId) st () markAsGuest = modifySession (\oldData -> oldData { vs_type = GuestSession }) -- | Replacement for 'readSession' readSessionData :: SpockAction conn (VisitorSession sess userId) st sess readSessionData = vs_data <$> readSession -- | Replacement for 'modifySession' modifySessionData :: (sess -> sess) -> SpockAction conn (VisitorSession sess userId) st () modifySessionData f = modifySession (\oldData -> oldData { vs_data = f (vs_data oldData) }) -- | Replacement for 'writeSession' writeSessionData :: sess -> SpockAction conn (VisitorSession sess userId) st () writeSessionData v = modifySessionData (const v) -- | Before the request is performed, you can check if the signed in user has permissions to -- view the contents of the request. You may want to define a helper function that -- proxies this function to not pass around 'NoAccessHandler', 'LoadUserFun' and 'CheckRightsFun' -- all the time. -- Example: -- -- > type MyWebMonad a = SpockAction Connection (VisitorSession () UserId) () a -- > newtype MyUser = MyUser { unMyUser :: T.Text } -- > -- > http403 msg = -- > do status Http.status403 -- > text (show msg) -- > -- > login :: Http.StdMethod -- > -> [UserRights] -- > -> RoutePattern -- > -> (MyUser -> MyWebMonad ()) -- > -> MyWebMonad () -- > login = -- > userRoute http403 myLoadUser myCheckRights -- userRoute :: NoAccessHandler conn sess userId st -> LoadUserFun conn sess userId st user -> CheckRightsFun conn sess userId st user -> Http.StdMethod -> [UserRights] -> T.Text -> (user -> SpockAction conn (VisitorSession sess userId) st ()) -> SpockM conn (VisitorSession sess userId) st () userRoute noAccessHandler loadUser checkRights reqTy requiredRights route action = defRoute reqTy route $ do sessData <- readSession case vs_type sessData of GuestSession -> noAccessHandler NotLoggedIn UserSession userId -> do mUser <- loadUser userId case mUser of Nothing -> noAccessHandler NotValidUser Just user -> do isOk <- checkRights user requiredRights if isOk then action user else noAccessHandler NotEnoughRights