Solidify design of BearerToken

This commit is contained in:
Gregor Kleen 2019-04-04 19:33:39 +02:00
parent 710b591b4a
commit 680b674b09
22 changed files with 363 additions and 48 deletions

View File

@ -27,6 +27,7 @@ notification-rate-limit: 3600
notification-collate-delay: 300 notification-collate-delay: 300
notification-expiration: 259201 notification-expiration: 259201
session-timeout: 7200 session-timeout: 7200
jwt-expiration: 604800
maximum-content-length: 52428800 maximum-content-length: 52428800
log-settings: log-settings:

View File

@ -117,6 +117,8 @@ dependencies:
- lattices - lattices
- hsass - hsass
- semigroupoids - semigroupoids
- jose-jwt
- mono-traversable
other-extensions: other-extensions:
- GeneralizedNewtypeDeriving - GeneralizedNewtypeDeriving

View File

@ -101,7 +101,7 @@ mkYesodDispatch "UniWorX" resourcesUniWorX
-- the place to put your migrate statements to have automatic database -- the place to put your migrate statements to have automatic database
-- migrations handled by Yesod. -- migrations handled by Yesod.
makeFoundation :: (MonadResource m, MonadBaseControl IO m) => AppSettings -> m UniWorX makeFoundation :: (MonadResource m, MonadBaseControl IO m) => AppSettings -> m UniWorX
makeFoundation appSettings@AppSettings{..} = do makeFoundation appSettings'@AppSettings{..} = do
-- Some basic initializations: HTTP connection manager, logger, and static -- Some basic initializations: HTTP connection manager, logger, and static
-- subsite. -- subsite.
appHttpManager <- newManager appHttpManager <- newManager
@ -141,7 +141,7 @@ makeFoundation appSettings@AppSettings{..} = do
-- logging function. To get out of this loop, we initially create a -- logging function. To get out of this loop, we initially create a
-- temporary foundation without a real connection pool, get a log function -- temporary foundation without a real connection pool, get a log function
-- from there, and then create the real foundation. -- from there, and then create the real foundation.
let mkFoundation appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached = UniWorX {..} let mkFoundation appConnPool appSmtpPool appLdapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached appJSONWebKeySet = UniWorX {..}
-- The UniWorX {..} syntax is an example of record wild cards. For more -- The UniWorX {..} syntax is an example of record wild cards. For more
-- information, see: -- information, see:
-- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html
@ -153,13 +153,14 @@ makeFoundation appSettings@AppSettings{..} = do
(error "sessionKey forced in tempFoundation") (error "sessionKey forced in tempFoundation")
(error "secretBoxKey forced in tempFoundation") (error "secretBoxKey forced in tempFoundation")
(error "widgetMemcached forced in tempFoundation") (error "widgetMemcached forced in tempFoundation")
(error "JSONWebKeySet forced in tempFoundation")
logFunc loc src lvl str = do logFunc loc src lvl str = do
f <- messageLoggerSource tempFoundation <$> readTVarIO (snd appLogger) f <- messageLoggerSource tempFoundation <$> readTVarIO (snd appLogger)
f loc src lvl str f loc src lvl str
flip runLoggingT logFunc $ do flip runLoggingT logFunc $ do
$logDebugS "InstanceID" $ UUID.toText appInstanceID $logDebugS "InstanceID" $ UUID.toText appInstanceID
-- logDebugS "Configuration" $ tshow appSettings -- logDebugS "Configuration" $ tshow appSettings'
smtpPool <- traverse createSmtpPool appSmtpConf smtpPool <- traverse createSmtpPool appSmtpConf
@ -177,8 +178,9 @@ makeFoundation appSettings@AppSettings{..} = do
appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool
appSessionKey <- clusterSetting (Proxy :: Proxy 'ClusterClientSessionKey) `runSqlPool` sqlPool appSessionKey <- clusterSetting (Proxy :: Proxy 'ClusterClientSessionKey) `runSqlPool` sqlPool
appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool appSecretBoxKey <- clusterSetting (Proxy :: Proxy 'ClusterSecretBoxKey) `runSqlPool` sqlPool
appJSONWebKeySet <- clusterSetting (Proxy :: Proxy 'ClusterJSONWebKeySet) `runSqlPool` sqlPool
let foundation = mkFoundation sqlPool smtpPool ldapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached let foundation = mkFoundation sqlPool smtpPool ldapPool appCryptoIDKey appSessionKey appSecretBoxKey appWidgetMemcached appJSONWebKeySet
handleJobs foundation handleJobs foundation
@ -265,7 +267,7 @@ makeLogWare app = do
logger <- readTVarIO . snd $ appLogger app logger <- readTVarIO . snd $ appLogger app
logWare <- mkRequestLogger def logWare <- mkRequestLogger def
{ outputFormat = bool { outputFormat = bool
(Apache . bool FromSocket FromHeader . appIpFromHeader $ appSettings app) (Apache . bool FromSocket FromHeader $ app ^. _appIpFromHeader)
(Detailed True) (Detailed True)
logDetailed logDetailed
, destination = Logger $ loggerSet logger , destination = Logger $ loggerSet logger
@ -287,8 +289,8 @@ makeLogWare app = do
-- | Warp settings for the given foundation value. -- | Warp settings for the given foundation value.
warpSettings :: UniWorX -> Settings warpSettings :: UniWorX -> Settings
warpSettings foundation = defaultSettings warpSettings foundation = defaultSettings
& setPort (appPort $ appSettings foundation) & setPort (foundation ^. _appPort)
& setHost (appHost $ appSettings foundation) & setHost (foundation ^. _appHost)
& setOnException (\_req e -> & setOnException (\_req e ->
when (defaultShouldDisplayException e) $ do when (defaultShouldDisplayException e) $ do
logger <- readTVarIO . snd $ appLogger foundation logger <- readTVarIO . snd $ appLogger foundation
@ -384,6 +386,6 @@ addPWEntry :: User
-> Text {-^ Password -} -> Text {-^ Password -}
-> IO () -> IO ()
addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db $ do addPWEntry User{ userAuthentication = _, ..} (Text.encodeUtf8 -> pw) = db $ do
PWHashConf{..} <- getsYesod $ appAuthPWHash . appSettings PWHashConf{..} <- getsYesod $ view _appAuthPWHash
(AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength (AuthPWHash . Text.decodeUtf8 -> userAuthentication) <- liftIO $ makePasswordWith pwHashAlgorithm pw pwHashStrength
void $ insert User{..} void $ insert User{..}

View File

@ -159,7 +159,7 @@ campusUser conf@LdapConf{..} pool Creds{..} = liftIO . (`catches` errHandlers) $
] ]
-- ldapConfig :: UniWorX -> LDAPConfig -- ldapConfig :: UniWorX -> LDAPConfig
-- ldapConfig _app@(appSettings -> settings) = LDAPConfig -- ldapConfig _app@(appSettings' -> settings) = LDAPConfig
-- { usernameFilter = \u -> principalName <> "=" <> u -- { usernameFilter = \u -> principalName <> "=" <> u
-- , identifierModifier -- , identifierModifier
-- , ldapUri = appLDAPURI settings -- , ldapUri = appLDAPURI settings

View File

@ -29,6 +29,11 @@ import Data.Aeson (ToJSON(..), ToJSONKey(..), ToJSONKeyFunction(..), FromJSON(..
import Data.Aeson.Encoding (text) import Data.Aeson.Encoding (text)
instance {-# OVERLAPPING #-} MonadThrow m => MonadCrypto (ReaderT CryptoIDKey m) where
type MonadCryptoKey (ReaderT CryptoIDKey m) = CryptoIDKey
cryptoIDKey f = ask >>= f
-- Generates CryptoUUID... and CryptoFileName... Datatypes -- Generates CryptoUUID... and CryptoFileName... Datatypes
decCryptoIDs [ ''SubmissionId decCryptoIDs [ ''SubmissionId
, ''FileId , ''FileId

View File

@ -0,0 +1,14 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.Aeson.Types.Instances
(
) where
import ClassyPrelude
import Data.Aeson.Types (Parser)
import Control.Monad.Catch
instance MonadThrow Parser where
throwM = fail . show

View File

@ -0,0 +1,20 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.NonNull.Instances
(
) where
import ClassyPrelude
import Data.Aeson
instance ToJSON a => ToJSON (NonNull a) where
toJSON = toJSON . toNullable
instance (FromJSON a, MonoFoldable a) => FromJSON (NonNull a) where
parseJSON = parseJSON >=> maybe (fail "Expected non-empty structure") return . fromNullable
instance Hashable a => Hashable (NonNull a) where
hashWithSalt s = hashWithSalt s . toNullable

View File

@ -77,6 +77,7 @@ import qualified Yesod.Auth.Message as Auth
import qualified Data.Conduit.List as C import qualified Data.Conduit.List as C
import qualified Crypto.Saltine.Core.SecretBox as SecretBox import qualified Crypto.Saltine.Core.SecretBox as SecretBox
import qualified Jose.Jwk as Jose
import qualified Database.Memcached.Binary.IO as Memcached import qualified Database.Memcached.Binary.IO as Memcached
import Data.Bits (Bits(zeroBits)) import Data.Bits (Bits(zeroBits))
@ -96,6 +97,8 @@ instance DisplayAble TermId where
instance DisplayAble SchoolId where instance DisplayAble SchoolId where
display = CI.original . unSchoolKey display = CI.original . unSchoolKey
type SMTPPool = Pool SMTPConnection
-- infixl 9 :$: -- infixl 9 :$:
-- pattern a :$: b = a b -- pattern a :$: b = a b
@ -104,7 +107,7 @@ instance DisplayAble SchoolId where
-- starts running, such as database connections. Every handler will have -- starts running, such as database connections. Every handler will have
-- access to the data present here. -- access to the data present here.
data UniWorX = UniWorX data UniWorX = UniWorX
{ appSettings :: AppSettings { appSettings' :: AppSettings
, appStatic :: EmbeddedStatic -- ^ Settings for static file serving. , appStatic :: EmbeddedStatic -- ^ Settings for static file serving.
, appConnPool :: ConnectionPool -- ^ Database connection pool. , appConnPool :: ConnectionPool -- ^ Database connection pool.
, appSmtpPool :: Maybe SMTPPool , appSmtpPool :: Maybe SMTPPool
@ -119,9 +122,16 @@ data UniWorX = UniWorX
, appCronThread :: TMVar (ReleaseKey, ThreadId) , appCronThread :: TMVar (ReleaseKey, ThreadId)
, appSessionKey :: ClientSession.Key , appSessionKey :: ClientSession.Key
, appSecretBoxKey :: SecretBox.Key , appSecretBoxKey :: SecretBox.Key
, appJSONWebKeySet :: Jose.JwkSet
} }
type SMTPPool = Pool SMTPConnection makeLenses_ ''UniWorX
instance HasInstanceID UniWorX InstanceId where
instanceID = _appInstanceID
instance HasJSONWebKeySet UniWorX Jose.JwkSet where
jsonWebKeySet = _appJSONWebKeySet
instance HasAppSettings UniWorX where
appSettings = _appSettings'
-- This is where we define all of the routes in our application. For a full -- This is where we define all of the routes in our application. For a full
-- explanation of the syntax, please see: -- explanation of the syntax, please see:
@ -463,7 +473,7 @@ tagAccessPredicate AuthNoEscalation = APDB $ \mAuthId route _ -> case route of
tagAccessPredicate AuthDeprecated = APHandler $ \_ r _ -> do tagAccessPredicate AuthDeprecated = APHandler $ \_ r _ -> do
$logWarnS "AccessControl" ("deprecated route: " <> tshow r) $logWarnS "AccessControl" ("deprecated route: " <> tshow r)
addMessageI Error MsgDeprecatedRoute addMessageI Error MsgDeprecatedRoute
allow <- appAllowDeprecated . appSettings <$> getYesod allow <- view _appAllowDeprecated
return $ bool (Unauthorized "Deprecated Route") Authorized allow return $ bool (Unauthorized "Deprecated Route") Authorized allow
tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do tagAccessPredicate AuthDevelopment = APHandler $ \_ r _ -> do
$logWarnS "AccessControl" ("route in development: " <> tshow r) $logWarnS "AccessControl" ("route in development: " <> tshow r)
@ -809,17 +819,17 @@ instance Yesod UniWorX where
-- Controls the base of generated URLs. For more information on modifying, -- Controls the base of generated URLs. For more information on modifying,
-- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot -- see: https://github.com/yesodweb/yesod/wiki/Overriding-approot
approot = ApprootRequest $ \app req -> approot = ApprootRequest $ \app req ->
case appRoot $ appSettings app of case app ^. _appRoot of
Nothing -> getApprootText guessApproot app req Nothing -> getApprootText guessApproot app req
Just root -> root Just root -> root
-- Store session data on the client in encrypted cookies, -- Store session data on the client in encrypted cookies,
-- default session idle timeout is 120 minutes -- default session idle timeout is 120 minutes
makeSessionBackend UniWorX{appSessionKey,appSettings=AppSettings{appSessionTimeout}} = do makeSessionBackend app = do
(getCachedDate, _) <- clientSessionDateCacher appSessionTimeout (getCachedDate, _) <- clientSessionDateCacher (app ^. _appSessionTimeout)
return . Just $ clientSessionBackend appSessionKey getCachedDate return . Just $ clientSessionBackend (app ^. _appSessionKey) getCachedDate
maximumContentLength UniWorX{appSettings=AppSettings{appMaximumContentLength}} _ = appMaximumContentLength maximumContentLength app _ = app ^. _appMaximumContentLength
-- Yesod Middleware allows you to run code before and after each handler function. -- Yesod Middleware allows you to run code before and after each handler function.
-- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks. -- The defaultYesodMiddleware adds the response header "Vary: Accept, Accept-Language" and performs authorization checks.
@ -878,7 +888,7 @@ instance Yesod UniWorX where
encrypted :: ToJSON a => a -> Widget -> Widget encrypted :: ToJSON a => a -> Widget -> Widget
encrypted plaintextJson plaintext = do encrypted plaintextJson plaintext = do
canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True
shouldEncrypt <- getsYesod $ appEncryptErrors . appSettings shouldEncrypt <- view _appEncryptErrors
if if
| shouldEncrypt | shouldEncrypt
, not canDecrypt -> do , not canDecrypt -> do
@ -919,8 +929,8 @@ instance Yesod UniWorX where
isAuthorized = evalAccess isAuthorized = evalAccess
addStaticContent ext _mime content = do addStaticContent ext _mime content = do
UniWorX{appWidgetMemcached, appSettings} <- getYesod UniWorX{appWidgetMemcached, appSettings'} <- getYesod
for ((,) <$> appWidgetMemcached <*> appWidgetMemcachedConf appSettings) $ \(mConn, WidgetMemcachedConf{ widgetMemcachedConnectInfo = _, .. }) -> do for ((,) <$> appWidgetMemcached <*> appWidgetMemcachedConf appSettings') $ \(mConn, WidgetMemcachedConf{ widgetMemcachedConnectInfo = _, .. }) -> do
let expiry = (maybe 0 ceiling widgetMemcachedExpiry) let expiry = (maybe 0 ceiling widgetMemcachedExpiry)
touch = liftIO $ Memcached.touch expiry (encodeUtf8 $ pack fileName) mConn touch = liftIO $ Memcached.touch expiry (encodeUtf8 $ pack fileName) mConn
add = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn add = liftIO $ Memcached.add zeroBits expiry (encodeUtf8 $ pack fileName) content mConn
@ -971,8 +981,7 @@ siteLayout = siteLayout' . Just
siteLayout' :: Maybe Widget -- ^ Optionally override `pageHeading` siteLayout' :: Maybe Widget -- ^ Optionally override `pageHeading`
-> Widget -> Handler Html -> Widget -> Handler Html
siteLayout' headingOverride widget = do siteLayout' headingOverride widget = do
master <- getYesod AppSettings { appUserDefaults = UserDefaultConf{..}, .. } <- view appSettings
let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master
isModal <- hasCustomHeader HeaderIsModal isModal <- hasCustomHeader HeaderIsModal
@ -2081,7 +2090,7 @@ instance YesodAuth UniWorX where
_other -> return res _other -> return res
$logDebugS "auth" $ tshow Creds{..} $logDebugS "auth" $ tshow Creds{..}
UniWorX{ appSettings = AppSettings{ appUserDefaults = UserDefaultConf{..}, ..}, .. } <- getYesod UniWorX{ appSettings' = AppSettings{ appUserDefaults = UserDefaultConf{..}, ..}, .. } <- getYesod
flip catches excHandlers $ case (,) <$> appLdapConf <*> appLdapPool of flip catches excHandlers $ case (,) <$> appLdapConf <*> appLdapPool of
Just (ldapConf, ldapPool) -> fmap (either id id) . runExceptT $ do Just (ldapConf, ldapPool) -> fmap (either id id) . runExceptT $ do
@ -2200,7 +2209,7 @@ instance YesodAuth UniWorX where
where where
insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ()) insertMaybe key val = get key >>= maybe (insert_ val) (\_ -> return ())
authPlugins (UniWorX{ appSettings = AppSettings{..}, appLdapPool }) = catMaybes authPlugins (UniWorX{ appSettings' = AppSettings{..}, appLdapPool }) = catMaybes
[ campusLogin <$> appLdapConf <*> appLdapPool [ campusLogin <$> appLdapConf <*> appLdapPool
, Just . hashLogin $ pwHashAlgorithm appAuthPWHash , Just . hashLogin $ pwHashAlgorithm appAuthPWHash
, dummyLogin <$ guard appAuthDummyLogin , dummyLogin <$ guard appAuthDummyLogin
@ -2225,9 +2234,9 @@ unsafeHandler f h = do
instance YesodMail UniWorX where instance YesodMail UniWorX where
defaultFromAddress = getsYesod $ appMailFrom . appSettings defaultFromAddress = getsYesod $ view _appMailFrom
mailObjectIdDomain = getsYesod $ appMailObjectDomain . appSettings mailObjectIdDomain = getsYesod $ view _appMailObjectDomain
mailVerp = getsYesod $ appMailVerp . appSettings mailVerp = getsYesod $ view _appMailVerp
mailDateTZ = return appTZ mailDateTZ = return appTZ
mailSmtp act = do mailSmtp act = do
pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool pool <- maybe (throwM MailNotAvailable) return =<< getsYesod appSmtpPool

View File

@ -4,6 +4,8 @@ module Handler.Utils
import Import import Import
import Utils.Lens
import qualified Data.Text as T import qualified Data.Text as T
-- import qualified Data.Set (Set) -- import qualified Data.Set (Set)
import qualified Data.Set as Set import qualified Data.Set as Set
@ -40,7 +42,7 @@ downloadFiles = do
case mauth of case mauth of
Just (Entity _ User{..}) -> return userDownloadFiles Just (Entity _ User{..}) -> return userDownloadFiles
Nothing -> do Nothing -> do
AppSettings{ appUserDefaults = UserDefaultConf{..} } <- getsYesod appSettings UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
return userDefaultDownloadFiles return userDefaultDownloadFiles
tidFromText :: Text -> Maybe TermId tidFromText :: Text -> Maybe TermId

View File

@ -13,6 +13,8 @@ module Handler.Utils.DateTime
import Import import Import
import Utils.Lens
import Data.Time.Zones import Data.Time.Zones
import qualified Data.Time.Zones as TZ import qualified Data.Time.Zones as TZ
@ -83,7 +85,7 @@ getTimeLocale = getTimeLocale' <$> languages
getDateTimeFormat :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m DateTimeFormat getDateTimeFormat :: (MonadHandler m, HandlerSite m ~ UniWorX) => SelDateTimeFormat -> m DateTimeFormat
getDateTimeFormat sel = do getDateTimeFormat sel = do
mauth <- liftHandlerT maybeAuth mauth <- liftHandlerT maybeAuth
AppSettings{ appUserDefaults = UserDefaultConf{..} } <- getsYesod appSettings UserDefaultConf{..} <- getsYesod $ view _appUserDefaults
let let
fmt fmt
| Just (Entity _ User{..}) <- mauth | Just (Entity _ User{..}) <- mauth
@ -182,4 +184,4 @@ weeksToAdd old new = loop 0 old
where where
loop n t loop n t
| t > new = n | t > new = n
| otherwise = loop (succ n) (addOneWeek t) | otherwise = loop (succ n) (addOneWeek t)

View File

@ -43,6 +43,7 @@ import GHC.Exts as Import (IsList)
import Data.Hashable as Import import Data.Hashable as Import
import Data.List.NonEmpty as Import (NonEmpty(..)) import Data.List.NonEmpty as Import (NonEmpty(..))
import Data.List.NonEmpty.Instances as Import () import Data.List.NonEmpty.Instances as Import ()
import Data.NonNull.Instances as Import ()
import Data.Text.Encoding.Error as Import(UnicodeException(..)) import Data.Text.Encoding.Error as Import(UnicodeException(..))
import Data.Semigroup as Import (Semigroup) import Data.Semigroup as Import (Semigroup)
import Data.Monoid as Import (Last(..), First(..)) import Data.Monoid as Import (Last(..), First(..))
@ -56,6 +57,7 @@ import Control.Monad.Trans.Resource as Import (ReleaseKey)
import Network.Mail.Mime.Instances as Import () import Network.Mail.Mime.Instances as Import ()
import Yesod.Core.Instances as Import () import Yesod.Core.Instances as Import ()
import Data.Aeson.Types.Instances as Import ()
import Ldap.Client.Pool as Import import Ldap.Client.Pool as Import
@ -67,6 +69,8 @@ import Numeric.Natural.Instances as Import ()
import System.Random as Import (Random) import System.Random as Import (Random)
import Control.Monad.Random.Class as Import (MonadRandom(..)) import Control.Monad.Random.Class as Import (MonadRandom(..))
import Jose.Jwt.Instances as Import ()
import Control.Monad.Trans.RWS (RWST) import Control.Monad.Trans.RWS (RWST)

View File

@ -6,6 +6,7 @@ module Jobs
) where ) where
import Import import Import
import Utils.Lens
import Jobs.Types as Types hiding (JobCtl(JobCtlQueue)) import Jobs.Types as Types hiding (JobCtl(JobCtlQueue))
import Jobs.Types (JobCtl(JobCtlQueue)) import Jobs.Types (JobCtl(JobCtlQueue))
@ -77,7 +78,7 @@ handleJobs :: ( MonadResource m
-- Uses `unsafeHandler`, as per documentation all HTTP-related fields of state/environment are meaningless placeholders. -- Uses `unsafeHandler`, as per documentation all HTTP-related fields of state/environment are meaningless placeholders.
-- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ... -- Handling commands in `HandlerT` provides us with the facilities to render urls, unifies logging, provides a value of the foundation type, ...
handleJobs foundation@UniWorX{..} = do handleJobs foundation@UniWorX{..} = do
let num = appJobWorkers appSettings let num = foundation ^. _appJobWorkers
jobCrontab <- liftIO $ newTMVarIO HashMap.empty jobCrontab <- liftIO $ newTMVarIO HashMap.empty
jobConfirm <- liftIO $ newTVarIO HashMap.empty jobConfirm <- liftIO $ newTVarIO HashMap.empty
@ -135,7 +136,7 @@ execCrontab = evalStateT go HashMap.empty
runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ merge runConduit $ transPipe lift (selectSource [] []) .| C.mapM_ merge
refT <- liftIO getCurrentTime refT <- liftIO getCurrentTime
settings <- getsYesod appSettings settings <- getsYesod appSettings'
currentState <- mapStateT (mapReaderT $ liftIO . atomically) $ do currentState <- mapStateT (mapReaderT $ liftIO . atomically) $ do
crontab' <- liftBase . tryReadTMVar =<< asks jobCrontab crontab' <- liftBase . tryReadTMVar =<< asks jobCrontab
case crontab' of case crontab' of
@ -157,7 +158,7 @@ execCrontab = evalStateT go HashMap.empty
| ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab | ((==) `on` HashMap.lookup jobCtl) newCrontab currentCrontab
-> do -> do
now <- liftIO $ getCurrentTime now <- liftIO $ getCurrentTime
instanceID <- getsYesod appInstanceID instanceID' <- getsYesod appInstanceID
State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl State.modify $ HashMap.alter (Just . ($ Max now) . maybe id (<>)) jobCtl
case jobCtl of case jobCtl of
JobCtlQueue job -> do JobCtlQueue job -> do
@ -166,7 +167,7 @@ execCrontab = evalStateT go HashMap.empty
CronLastExec CronLastExec
{ cronLastExecJob = toJSON job { cronLastExecJob = toJSON job
, cronLastExecTime = now , cronLastExecTime = now
, cronLastExecInstance = instanceID , cronLastExecInstance = instanceID'
} }
[ CronLastExecTime =. now ] [ CronLastExecTime =. now ]
lift . lift $ queueDBJob job lift . lift $ queueDBJob job
@ -285,21 +286,21 @@ jLocked jId act = do
let let
lock = runDB . setSerializable $ do lock = runDB . setSerializable $ do
qj@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId qj@QueuedJob{..} <- maybe (throwM $ JNonexistant jId) return =<< get jId
instanceID <- getsYesod appInstanceID instanceID' <- getsYesod $ view instanceID
threshold <- getsYesod $ appJobStaleThreshold . appSettings threshold <- getsYesod $ view _appJobStaleThreshold
now <- liftIO getCurrentTime now <- liftIO getCurrentTime
hadStale <- maybeT (return False) $ do hadStale <- maybeT (return False) $ do
lockTime <- MaybeT $ return queuedJobLockTime lockTime <- MaybeT $ return queuedJobLockTime
lockInstance <- MaybeT $ return queuedJobLockInstance lockInstance <- MaybeT $ return queuedJobLockInstance
if if
| lockInstance == instanceID | lockInstance == instanceID'
, diffUTCTime now lockTime >= threshold , diffUTCTime now lockTime >= threshold
-> return True -> return True
| otherwise | otherwise
-> throwM $ JLocked jId lockInstance lockTime -> throwM $ JLocked jId lockInstance lockTime
when hadStale . when hadStale .
$logWarnS "Jobs" $ "Ignored stale lock: " <> tshow qj $logWarnS "Jobs" $ "Ignored stale lock: " <> tshow qj
val <- updateGet jId [ QueuedJobLockInstance =. Just instanceID val <- updateGet jId [ QueuedJobLockInstance =. Just instanceID'
, QueuedJobLockTime =. Just now , QueuedJobLockTime =. Just now
] ]
liftIO . atomically $ writeTVar hasLock True liftIO . atomically $ writeTVar hasLock True

View File

@ -23,7 +23,7 @@ import qualified Data.Conduit.List as C
determineCrontab :: DB (Crontab JobCtl) determineCrontab :: DB (Crontab JobCtl)
-- ^ Extract all future jobs from the database (sheet deadlines, ...) -- ^ Extract all future jobs from the database (sheet deadlines, ...)
determineCrontab = execWriterT $ do determineCrontab = execWriterT $ do
AppSettings{..} <- getsYesod appSettings AppSettings{..} <- getsYesod appSettings'
case appJobFlushInterval of case appJobFlushInterval of
Just interval -> tell $ HashMap.singleton Just interval -> tell $ HashMap.singleton

View File

@ -20,7 +20,7 @@ dispatchJobHelpRequest :: Either (Maybe Address) UserId
-> Maybe Text -- ^ Referer -> Maybe Text -- ^ Referer
-> Handler () -> Handler ()
dispatchJobHelpRequest jSender jRequestTime jHelpRequest jReferer = do dispatchJobHelpRequest jSender jRequestTime jHelpRequest jReferer = do
supportAddress <- getsYesod $ appMailSupport . appSettings supportAddress <- view _appMailSupport
userInfo <- bitraverse return (runDB . getEntity) jSender userInfo <- bitraverse return (runDB . getEntity) jSender
let userAddress = either let userAddress = either
id id

18
src/Jose/Jwt/Instances.hs Normal file
View File

@ -0,0 +1,18 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Jose.Jwt.Instances
(
) where
import ClassyPrelude.Yesod
import Jose.Jwt
instance PathPiece Jwt where
toPathPiece (Jwt bytes) = decodeUtf8 bytes
fromPathPiece = Just . Jwt . encodeUtf8
deriving instance Typeable JwtError
instance Exception JwtError

View File

@ -2,7 +2,6 @@ module Model.Migration.Types where
import ClassyPrelude.Yesod import ClassyPrelude.Yesod
import Data.Aeson.TH (deriveJSON, defaultOptions) import Data.Aeson.TH (deriveJSON, defaultOptions)
import Database.Persist.Sql
import qualified Model as Current import qualified Model as Current
import qualified Model.Types.JSON as Current import qualified Model.Types.JSON as Current

158
src/Model/Token.hs Normal file
View File

@ -0,0 +1,158 @@
{-# LANGUAGE UndecidableInstances #-}
module Model.Token
( BearerToken(..)
, bearerToken
, encodeToken, decodeToken
, tokenToJSON, tokenParseJSON
) where
import ClassyPrelude.Yesod
import Model
import Settings
import Utils.Lens hiding ((.=))
import Yesod.Auth (AuthId)
-- import qualified Jose.Jwa as Jose
import Jose.Jwk (JwkSet)
-- import qualified Jose.Jwk as Jose
import Jose.Jwt (Jwt, JwtError, IntDate(..))
import qualified Jose.Jwt as Jose
import Jose.Jwt.Instances ()
import Data.Aeson.Types.Instances ()
import qualified Crypto.Random as Crypto (MonadRandom)
import Data.HashSet (HashSet)
import qualified Data.HashMap.Strict as HashMap
import Data.Aeson.Types (Parser, (.:?), (.:))
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import CryptoID
import Data.Time.Clock
import Data.Time.Clock.POSIX
import Control.Monad.Random (MonadRandom(..))
data BearerToken site = BearerToken
{ tokenIdentifier :: TokenId
, tokenAuthority :: AuthId site
, tokenRoutes :: Maybe (HashSet (Route site))
, tokenAddAuth :: AuthCNF
, tokenIssuedAt :: UTCTime
, tokenIssuedBy :: InstanceId
, tokenExpiresAt
, tokenStartsAt :: Maybe UTCTime
} deriving (Generic, Typeable)
deriving instance (Eq (AuthId site), Eq (Route site)) => Eq (BearerToken site)
deriving instance (Read (AuthId site), Eq (Route site), Hashable (Route site), Read (Route site)) => Read (BearerToken site)
deriving instance (Show (AuthId site), Show (Route site)) => Show (BearerToken site)
tokenToJSON :: forall m.
( MonadHandler m
, HasCryptoUUID (AuthId (HandlerSite m)) m
, RenderRoute (HandlerSite m)
) => BearerToken (HandlerSite m) -> m Value
tokenToJSON BearerToken{..} = do
cID <- encrypt tokenAuthority :: m (CryptoUUID (AuthId (HandlerSite m)))
let stdPayload = Jose.JwtClaims
{ jwtIss = Just $ toPathPiece tokenIssuedBy
, jwtSub = Nothing
, jwtAud = Nothing
, jwtExp = IntDate . utcTimeToPOSIXSeconds <$> tokenExpiresAt
, jwtNbf = IntDate . utcTimeToPOSIXSeconds <$> tokenStartsAt
, jwtIat = Just . IntDate $ utcTimeToPOSIXSeconds tokenIssuedAt
, jwtJti = Just $ toPathPiece tokenIdentifier
}
return . JSON.object $
[ "authority" .= cID
, "routes" .= tokenRoutes
, "add-auth" .= tokenAddAuth
] ++ let JSON.Object hm = toJSON stdPayload in HashMap.toList hm
tokenParseJSON :: forall site.
( HasCryptoUUID (AuthId site) (ReaderT CryptoIDKey Parser)
, ParseRoute site
, Hashable (Route site)
)
=> Value
-> ReaderT CryptoIDKey Parser (BearerToken site)
tokenParseJSON v@(Object o) = do
tokenAuthority' <- lift (o .: "authority") :: ReaderT CryptoIDKey Parser (CryptoUUID (AuthId site))
tokenAuthority <- decrypt tokenAuthority'
tokenRoutes <- lift $ o .:? "routes"
tokenAddAuth <- lift $ o .: "add-auth"
Jose.JwtClaims{..} <- lift $ parseJSON v
let unIntDate (IntDate posix) = posixSecondsToUTCTime posix
Just tokenIssuedBy <- return $ jwtIss >>= fromPathPiece
Just tokenIdentifier <- return $ jwtJti >>= fromPathPiece
Just tokenIssuedAt <- return $ unIntDate <$> jwtIat
let tokenExpiresAt = unIntDate <$> jwtExp
tokenStartsAt = unIntDate <$> jwtNbf
return BearerToken{..}
tokenParseJSON v = lift $ JSON.typeMismatch "BearerToken" v
bearerToken :: forall m.
( MonadHandler m
, HasInstanceID (HandlerSite m) InstanceId
, HasCryptoUUID (AuthId (HandlerSite m)) m
, HasAppSettings (HandlerSite m)
)
=> AuthId (HandlerSite m)
-> Maybe (HashSet (Route (HandlerSite m)))
-> AuthCNF
-> Maybe (Maybe UTCTime) -- ^ @Nothing@ determines default expiry time automatically
-> Maybe UTCTime -- ^ @Nothing@ means token starts to be valid immediately
-> m (BearerToken (HandlerSite m))
bearerToken tokenAuthority tokenRoutes tokenAddAuth mTokenExpiresAt tokenStartsAt = do
tokenIdentifier <- liftIO $ getRandom
tokenIssuedAt <- liftIO $ getCurrentTime
tokenIssuedBy <- getsYesod $ view instanceID
defaultExpiration <- getsYesod $ view _appJWTExpiration
let tokenExpiresAt
| Just t <- mTokenExpiresAt
= t
| Just tDiff <- defaultExpiration
= Just $ tDiff `addUTCTime` fromMaybe tokenIssuedAt tokenStartsAt
| otherwise
= Nothing
return BearerToken{..}
encodeToken :: forall m.
( Crypto.MonadRandom m
, MonadHandler m
, HasJSONWebKeySet (HandlerSite m) JwkSet
, HasInstanceID (HandlerSite m) InstanceId
, HasCryptoUUID (AuthId (HandlerSite m)) m
, RenderRoute (HandlerSite m)
)
=> BearerToken (HandlerSite m) -> m Jwt
encodeToken token = do
_payload <- tokenToJSON token
error "Not implemented"
decodeToken :: forall m.
( Crypto.MonadRandom m
, MonadHandler m
, HasJSONWebKeySet (HandlerSite m) JwkSet
, HasCryptoUUID (AuthId (HandlerSite m)) m
)
=> Jwt -> m (Either JwtError (BearerToken (HandlerSite m)))
decodeToken = error "Not implemented"

View File

@ -27,6 +27,8 @@ import Data.Universe.Helpers
import Data.UUID.Types (UUID) import Data.UUID.Types (UUID)
import qualified Data.UUID.Types as UUID import qualified Data.UUID.Types as UUID
import Data.NonNull.Instances ()
import Data.Default import Data.Default
import Text.Read (readMaybe) import Text.Read (readMaybe)
@ -54,7 +56,7 @@ import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson import qualified Data.Aeson.Types as Aeson
import Data.Aeson (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), FromJSONKeyFunction(..), withText, withObject, Value()) import Data.Aeson (FromJSON(..), ToJSON(..), FromJSONKey(..), ToJSONKey(..), FromJSONKeyFunction(..), withText, withObject, Value())
import Data.Aeson.Types (toJSONKeyText) import Data.Aeson.Types (toJSONKeyText)
import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..)) import Data.Aeson.TH (deriveJSON, defaultOptions, Options(..), SumEncoding(..), mkToJSON, mkParseJSON)
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Generics.Deriving.Monoid (memptydefault, mappenddefault) import Generics.Deriving.Monoid (memptydefault, mappenddefault)
@ -79,7 +81,7 @@ import Model.Types.Wordlist
import Data.Text.Metrics (damerauLevenshtein) import Data.Text.Metrics (damerauLevenshtein)
import Data.Binary (Binary) import Data.Binary (Binary)
instance PathPiece UUID where instance PathPiece UUID where
fromPathPiece = UUID.fromString . unpack fromPathPiece = UUID.fromString . unpack
@ -731,7 +733,7 @@ data AuthTag -- sortiert nach gewünschter Reihenfolge auf /authpreds, d.h. Prä
| AuthDeprecated | AuthDeprecated
| AuthDevelopment | AuthDevelopment
| AuthFree | AuthFree
deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic) deriving (Eq, Ord, Enum, Bounded, Read, Show, Generic, Typeable)
instance Universe AuthTag instance Universe AuthTag
instance Finite AuthTag instance Finite AuthTag
@ -774,6 +776,40 @@ instance FromJSON AuthTagActive where
derivePersistFieldJSON ''AuthTagActive derivePersistFieldJSON ''AuthTagActive
data PredLiteral a = PLVariable { plVar :: a } | PLNegated { plVar :: a }
deriving (Eq, Ord, Read, Show, Generic, Typeable)
instance Hashable a => Hashable (PredLiteral a)
deriveJSON defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, sumEncoding = ObjectWithSingleField
, unwrapUnaryRecords = True
} ''PredLiteral
newtype PredCNF a = PredCNF (Set (NonNull (Set (PredLiteral a))))
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving newtype (Semigroup, Monoid)
newtype PredDNF a = PredDNF (Set (NonNull (Set (PredLiteral a))))
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving newtype (Semigroup, Monoid)
$(return [])
instance (Ord a, ToJSON a) => ToJSON (PredCNF a) where
toJSON = $(mkToJSON predNFAesonOptions ''PredCNF)
instance (Ord a, FromJSON a) => FromJSON (PredCNF a) where
parseJSON = $(mkParseJSON predNFAesonOptions ''PredCNF)
instance (Ord a, ToJSON a) => ToJSON (PredDNF a) where
toJSON = $(mkToJSON predNFAesonOptions ''PredDNF)
instance (Ord a, FromJSON a) => FromJSON (PredDNF a) where
parseJSON = $(mkParseJSON predNFAesonOptions ''PredDNF)
type AuthCNF = PredCNF AuthTag
type AuthDNF = PredDNF AuthTag
data LecturerType = CourseLecturer | CourseAssistant data LecturerType = CourseLecturer | CourseAssistant
deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable) deriving (Eq, Ord, Read, Show, Enum, Bounded, Generic, Typeable)
@ -801,4 +837,5 @@ type UserEmail = CI Email
type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString type PWHashAlgorithm = ByteString -> PWStore.Salt -> Int -> ByteString
type InstanceId = UUID type InstanceId = UUID
type TokenId = UUID
type TermCandidateIncidence = UUID type TermCandidateIncidence = UUID

View File

@ -1,5 +1,6 @@
module Model.Types.JSON module Model.Types.JSON
( derivePersistFieldJSON ( derivePersistFieldJSON
, predNFAesonOptions
) where ) where
import ClassyPrelude.Yesod hiding (derivePersistFieldJSON) import ClassyPrelude.Yesod hiding (derivePersistFieldJSON)
@ -9,11 +10,13 @@ import Database.Persist.Sql
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text.Encoding as Text import qualified Data.Text.Encoding as Text
import qualified Data.Aeson as JSON import Data.Aeson as JSON
import Language.Haskell.TH import Language.Haskell.TH
import Language.Haskell.TH.Datatype import Language.Haskell.TH.Datatype
import Utils.PathPiece
derivePersistFieldJSON :: Name -> DecsQ derivePersistFieldJSON :: Name -> DecsQ
derivePersistFieldJSON tName = do derivePersistFieldJSON tName = do
@ -28,10 +31,10 @@ derivePersistFieldJSON tName = do
| otherwise = cxt [[t|PersistField|] `appT` t] | otherwise = cxt [[t|PersistField|] `appT` t]
sequence sequence
[ instanceD iCxt ([t|PersistField|] `appT` t) [ instanceD iCxt ([t|PersistField|] `appT` t)
[ funD (mkName "toPersistValue") [ funD 'toPersistValue
[ clause [] (normalB [e|PersistDbSpecific . LBS.toStrict . JSON.encode|]) [] [ clause [] (normalB [e|PersistDbSpecific . LBS.toStrict . JSON.encode|]) []
] ]
, funD (mkName "fromPersistValue") , funD 'fromPersistValue
[ do [ do
bs <- newName "bs" bs <- newName "bs"
clause [[p|PersistDbSpecific $(varP bs)|]] (normalB [e|first pack $ JSON.eitherDecodeStrict' $(varE bs)|]) [] clause [[p|PersistDbSpecific $(varP bs)|]] (normalB [e|first pack $ JSON.eitherDecodeStrict' $(varE bs)|]) []
@ -45,8 +48,20 @@ derivePersistFieldJSON tName = do
] ]
] ]
, instanceD sqlCxt ([t|PersistFieldSql|] `appT` t) , instanceD sqlCxt ([t|PersistFieldSql|] `appT` t)
[ funD (mkName "sqlType") [ funD 'sqlType
[ clause [wildP] (normalB [e|SqlOther "jsonb"|]) [] [ clause [wildP] (normalB [e|SqlOther "jsonb"|]) []
] ]
] ]
] ]
predNFAesonOptions :: Options
-- ^ Needed for JSON instances of `predCNF` and `predDNF`
--
-- Moved to this module due to stage restriction
predNFAesonOptions = defaultOptions
{ constructorTagModifier = camelToPathPiece' 1
, sumEncoding = ObjectWithSingleField
, tagSingleConstructors = True
}

View File

@ -100,6 +100,7 @@ data AppSettings = AppSettings
, appNotificationExpiration :: NominalDiffTime , appNotificationExpiration :: NominalDiffTime
, appSessionTimeout :: NominalDiffTime , appSessionTimeout :: NominalDiffTime
, appMaximumContentLength :: Maybe Word64 , appMaximumContentLength :: Maybe Word64
, appJWTExpiration :: Maybe NominalDiffTime
, appInitialLogSettings :: LogSettings , appInitialLogSettings :: LogSettings
@ -352,6 +353,7 @@ instance FromJSON AppSettings where
appNotificationRateLimit <- o .: "notification-rate-limit" appNotificationRateLimit <- o .: "notification-rate-limit"
appNotificationCollateDelay <- o .: "notification-collate-delay" appNotificationCollateDelay <- o .: "notification-collate-delay"
appNotificationExpiration <- o .: "notification-expiration" appNotificationExpiration <- o .: "notification-expiration"
appJWTExpiration <- o .:? "jwt-expiration"
appSessionTimeout <- o .: "session-timeout" appSessionTimeout <- o .: "session-timeout"
@ -379,6 +381,8 @@ instance FromJSON AppSettings where
return AppSettings {..} return AppSettings {..}
makeClassy_ ''AppSettings
-- | Settings for 'widgetFile', such as which template languages to support and -- | Settings for 'widgetFile', such as which template languages to support and
-- default Hamlet settings. -- default Hamlet settings.
-- --

View File

@ -32,11 +32,16 @@ import qualified Data.Binary as Binary
import qualified Data.Serialize as Serialize import qualified Data.Serialize as Serialize
import qualified Data.ByteString.Base64.URL as Base64 import qualified Data.ByteString.Base64.URL as Base64
import qualified Jose.Jwa as Jose
import qualified Jose.Jwk as Jose
import qualified Jose.Jwt as Jose
data ClusterSettingsKey data ClusterSettingsKey
= ClusterCryptoIDKey = ClusterCryptoIDKey
| ClusterClientSessionKey | ClusterClientSessionKey
| ClusterSecretBoxKey | ClusterSecretBoxKey
| ClusterJSONWebKeySet
deriving (Eq, Ord, Enum, Bounded, Show, Read) deriving (Eq, Ord, Enum, Bounded, Show, Read)
instance Universe ClusterSettingsKey instance Universe ClusterSettingsKey
@ -120,3 +125,12 @@ instance FromJSON SecretBox.Key where
parseJSON = Aeson.withText "Key" $ \t -> do parseJSON = Aeson.withText "Key" $ \t -> do
bytes <- either fail return . Base64.decode $ encodeUtf8 t bytes <- either fail return . Base64.decode $ encodeUtf8 t
maybe (fail "Could not parse key") return $ Saltine.decode bytes maybe (fail "Could not parse key") return $ Saltine.decode bytes
instance ClusterSetting 'ClusterJSONWebKeySet where
type ClusterSettingValue 'ClusterJSONWebKeySet = Jose.JwkSet
initClusterSetting _ = liftIO $ do
now <- getCurrentTime
jwkSig <- Jose.generateSymmetricKey 32 (Jose.UTCKeyId now) Jose.Sig (Just $ Jose.Signed Jose.HS256)
return $ Jose.JwkSet [jwkSig]
knownClusterSetting _ = ClusterJSONWebKeySet

View File

@ -93,4 +93,12 @@ makeLenses_ ''StudyTermCandidate
-- makeClassy_ ''Load -- makeClassy_ ''Load
--------------------------
-- Fields for `UniWorX` --
--------------------------
class HasInstanceID s a | s -> a where
instanceID :: Lens' s a
class HasJSONWebKeySet s a | s -> a where
jsonWebKeySet :: Lens' s a