/src/Authentication/AcidStateBackend.hs

https://bitbucket.org/eric_jones_/potion-soap-server · Haskell · 274 lines · 183 code · 57 blank · 34 comment · 9 complexity · 9a3c34ee40fb15c1867d389f30170211 MD5 · raw file

  1. {-# LANGUAGE TypeFamilies #-}
  2. {-# LANGUAGE FlexibleInstances #-}
  3. {-# LANGUAGE FlexibleContexts #-}
  4. {-# LANGUAGE DeriveDataTypeable #-}
  5. {-# LANGUAGE OverloadedStrings #-}
  6. {-# LANGUAGE StandaloneDeriving #-}
  7. {-# LANGUAGE TemplateHaskell #-}
  8. {-# OPTIONS_GHC -fno-warn-orphans #-}
  9. module Authentication.AcidStateBackend
  10. where
  11. import Control.Exception hiding (Handler)
  12. import Data.Acid
  13. import Data.Aeson (Value)
  14. import Data.Attoparsec.Number (Number)
  15. import Control.Lens
  16. import qualified Data.HashMap.Strict as H
  17. import Data.Hashable (Hashable)
  18. import Data.Maybe
  19. import Data.SafeCopy
  20. import Data.Text (Text, pack)
  21. import Data.Time
  22. import Data.Typeable (Typeable)
  23. import qualified Data.Vector as V (Vector, toList, fromList)
  24. import Snap
  25. import Snap.Snaplet.Auth
  26. import Snap.Snaplet.Session
  27. import System.Directory
  28. import System.IO.Error
  29. import Web.ClientSession
  30. import System.FilePath ((</>))
  31. ------------------------------------------------------------------------------
  32. type UserLogin = Text
  33. type RToken = Text
  34. ------------------------------------------------------------------------------
  35. data UserStore = UserStore
  36. { _users :: H.HashMap UserId AuthUser
  37. , _loginIndex :: H.HashMap UserLogin UserId
  38. , _tokenIndex :: H.HashMap RToken UserId
  39. , _nextUserId :: Int
  40. } deriving (Typeable)
  41. makeLenses ''UserStore
  42. ------------------------------------------------------------------------------
  43. instance (SafeCopy a, SafeCopy b, Eq a, Hashable a) =>
  44. SafeCopy (H.HashMap a b) where
  45. getCopy = contain $ fmap H.fromList safeGet
  46. putCopy = contain . safePut . H.toList
  47. ------------------------------------------------------------------------------
  48. instance (SafeCopy a) => SafeCopy (V.Vector a) where
  49. getCopy = contain $ fmap V.fromList safeGet
  50. putCopy = contain . safePut . V.toList
  51. ------------------------------------------------------------------------------
  52. deriving instance Typeable AuthUser
  53. ------------------------------------------------------------------------------
  54. $(deriveSafeCopy 0 'base ''Number)
  55. $(deriveSafeCopy 0 'base ''Value)
  56. $(deriveSafeCopy 0 'base ''Password)
  57. $(deriveSafeCopy 0 'base ''Role)
  58. $(deriveSafeCopy 0 'base ''AuthFailure)
  59. $(deriveSafeCopy 0 'base ''AuthUser)
  60. $(deriveSafeCopy 0 'base ''UserId)
  61. $(deriveSafeCopy 0 'base ''UserStore)
  62. ------------------------------------------------------------------------------
  63. emptyUS :: UserStore
  64. emptyUS = UserStore H.empty H.empty H.empty 0
  65. ------------------------------------------------------------------------------
  66. saveAuthUser :: AuthUser
  67. -> UTCTime
  68. -> Update UserStore (Either AuthFailure AuthUser)
  69. saveAuthUser user utcTime = do
  70. let authUserId = userId user
  71. case authUserId of
  72. Just uid -> saveExistingUser user uid utcTime
  73. Nothing -> saveNewUser user utcTime
  74. ------------------------------------------------------------------------------
  75. saveNewUser :: AuthUser
  76. -> UTCTime
  77. -> Update UserStore (Either AuthFailure AuthUser)
  78. saveNewUser user currentTime = do
  79. loginCache <- use loginIndex
  80. if isJust $ H.lookup (userLogin user) loginCache
  81. then return $ Left DuplicateLogin
  82. else do
  83. uid <- liftM (UserId . pack . show) $ use nextUserId
  84. nextUserId += 1
  85. let user' = user { userUpdatedAt = Just currentTime, userId = Just uid }
  86. updateUserCache user' uid
  87. updateLoginCache (userLogin user') uid
  88. updateTokenCache (userRememberToken user) uid
  89. return $ Right user'
  90. ------------------------------------------------------------------------------
  91. saveExistingUser :: AuthUser
  92. -> UserId
  93. -> UTCTime
  94. -> Update UserStore (Either AuthFailure AuthUser)
  95. saveExistingUser user uId currentTime = do
  96. loginCache <- use loginIndex
  97. if Just uId /= H.lookup (userLogin user) loginCache
  98. then return $ Left DuplicateLogin
  99. else do
  100. userCache <- use users
  101. let oldUser = fromMaybe user $ H.lookup uId userCache
  102. loginIndex %= H.delete (userLogin oldUser)
  103. tokenIndex %= deleteIfJust (userRememberToken oldUser)
  104. let user' = user { userUpdatedAt = Just currentTime }
  105. updateUserCache user' uId
  106. updateLoginCache (userLogin user') uId
  107. updateTokenCache (userRememberToken user) uId
  108. return $ Right user
  109. ------------------------------------------------------------------------------
  110. deleteIfJust :: (Hashable a, Eq a) => Maybe a -> H.HashMap a b -> H.HashMap a b
  111. deleteIfJust (Just val) hash = H.delete val hash
  112. deleteIfJust Nothing hash = hash
  113. ------------------------------------------------------------------------------
  114. updateUserCache :: (MonadState UserStore m) => AuthUser -> UserId -> m ()
  115. updateUserCache user uid = users %= H.insert uid user
  116. ------------------------------------------------------------------------------
  117. updateLoginCache :: (MonadState UserStore m) => Text-> UserId -> m ()
  118. updateLoginCache login uid = loginIndex %= H.insert login uid
  119. ------------------------------------------------------------------------------
  120. updateTokenCache :: (MonadState UserStore m) => Maybe Text -> UserId -> m ()
  121. updateTokenCache (Just token) uid = tokenIndex %= H.insert token uid
  122. updateTokenCache Nothing _ = return ()
  123. ------------------------------------------------------------------------------
  124. byUserId :: UserId -> Query UserStore (Maybe AuthUser)
  125. byUserId uid = do
  126. us <- view users
  127. return $ H.lookup uid us
  128. ------------------------------------------------------------------------------
  129. byLogin :: UserLogin -> Query UserStore (Maybe AuthUser)
  130. byLogin l = do
  131. li <- view loginIndex
  132. maybe (return Nothing) byUserId $ H.lookup l li
  133. ------------------------------------------------------------------------------
  134. byRememberToken :: RToken -> Query UserStore (Maybe AuthUser)
  135. byRememberToken tok = do
  136. ti <- view tokenIndex
  137. maybe (return Nothing) byUserId $ H.lookup tok ti
  138. ------------------------------------------------------------------------------
  139. destroyU :: AuthUser -> Update UserStore ()
  140. destroyU authUser =
  141. case userId authUser of
  142. Nothing -> return ()
  143. Just uid -> do
  144. UserStore us li ti n <- get
  145. storedUser <- runQuery $ byUserId uid
  146. let li' = fromMaybe li $
  147. H.delete . userLogin <$> storedUser <*> pure li
  148. ti' = fromMaybe ti $
  149. H.delete <$> (userRememberToken =<< storedUser) <*> pure ti
  150. put $ UserStore (H.delete uid us) li' ti' n
  151. ------------------------------------------------------------------------------
  152. allLogins :: Query UserStore [UserLogin]
  153. allLogins = do
  154. li <- view loginIndex
  155. return $ H.keys li
  156. ------------------------------------------------------------------------------
  157. $(makeAcidic ''UserStore [ 'saveAuthUser
  158. , 'byUserId
  159. , 'byLogin
  160. , 'byRememberToken
  161. , 'destroyU
  162. , 'allLogins
  163. ] )
  164. ------------------------------------------------------------------------------
  165. instance IAuthBackend (AcidState UserStore) where
  166. save = acidSave
  167. lookupByUserId acid uid = query acid $ ByUserId uid
  168. lookupByLogin acid l = query acid $ ByLogin l
  169. lookupByRememberToken acid tok = query acid $ ByRememberToken tok
  170. destroy acid authUser = update acid $ DestroyU authUser
  171. ------------------------------------------------------------------------------
  172. acidSave :: AcidState UserStore
  173. -> AuthUser
  174. -> IO (Either AuthFailure AuthUser)
  175. acidSave acid user = do
  176. currentTime <- getCurrentTime
  177. update acid $ SaveAuthUser user currentTime
  178. ------------------------------------------------------------------------------
  179. initAcidAuthManager :: AuthSettings
  180. -> SnapletLens b SessionManager
  181. -> SnapletInit b (AuthManager b)
  182. initAcidAuthManager s lns =
  183. makeSnaplet
  184. "AcidStateAuthManager"
  185. "A snaplet providing user authentication using an Acid State back-end"
  186. Nothing $ do
  187. removeResourceLockOnUnload
  188. rng <- liftIO mkRNG
  189. key <- liftIO $ getKey (asSiteKey s)
  190. sfp <- getSnapletFilePath
  191. acid <- liftIO $ openLocalStateFrom sfp emptyUS
  192. return AuthManager
  193. { backend = acid
  194. , session = lns
  195. , activeUser = Nothing
  196. , minPasswdLen = asMinPasswdLen s
  197. , rememberCookieName = asRememberCookieName s
  198. , rememberPeriod = asRememberPeriod s
  199. , siteKey = key
  200. , lockout = asLockout s
  201. , randomNumberGenerator = rng
  202. }
  203. ------------------------------------------------------------------------------
  204. removeResourceLockOnUnload :: Initializer b v ()
  205. removeResourceLockOnUnload = do
  206. snapletFilePath <- getSnapletFilePath
  207. let resourceLockPath = snapletFilePath </> "open.lock"
  208. onUnload $ removeIfExists resourceLockPath
  209. ------------------------------------------------------------------------------
  210. removeIfExists :: FilePath -> IO ()
  211. removeIfExists fileName = removeFile fileName `catch` handleExists
  212. where handleExists e
  213. | isDoesNotExistError e = return ()
  214. | otherwise = throwIO e
  215. ------------------------------------------------------------------------------
  216. getAllLogins :: AcidState UserStore -> Handler b (AuthManager v) [Text]
  217. getAllLogins acid = liftIO $ query acid AllLogins