diff --git a/.gitignore b/.gitignore index f744360b3..84a8fe8a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ dist* static/tmp/ static/combined/ -client_session_key.aes -cryptoid_key.bf *.hi *.o *.sqlite3 diff --git a/config/settings.yml b/config/settings.yml index f4602cd0e..373385dee 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -26,6 +26,7 @@ job-stale-threshold: 300 notification-rate-limit: 3600 notification-collate-delay: 300 notification-expiration: 259201 +session-timeout: 7200 log-settings: log-detailed: "_env:DETAILED_LOGGING:false" @@ -41,10 +42,12 @@ auth-pw-hash: strength: 14 # Optional values with the following production defaults. -# In development, they default to true. +# In development, they default to the opposite. # reload-templates: false # mutable-static: false # skip-combining: false +# encrypt-errors: true +encrypt-errors: true database: user: "_env:PGUSER:uniworx" @@ -86,5 +89,4 @@ user-defaults: time-format: "%R" download-files: false -cryptoid-keyfile: "_env:CRYPTOID_KEYFILE:cryptoid_key.bf" -instance-idfile: "_env:INSTANCEID_FILE:instance" +instance-idfile: "_env:INSTANCE_ID:instance" diff --git a/messages/uniworx/de.msg b/messages/uniworx/de.msg index 898b4385a..da3c0a31e 100644 --- a/messages/uniworx/de.msg +++ b/messages/uniworx/de.msg @@ -376,7 +376,7 @@ NotificationTriggerSubmissionRated: Meine Abgabe wurde korrigiert NotificationTriggerSheetActive: Ich kann ein neues Übungsblatt herunterladen NotificationTriggerSheetSoonInactive: Ich kann ein Übungsblatt bald nicht mehr abgeben NotificationTriggerSheetInactive: Abgabefrist eines meiner Übungsblätter ist abgelaufen -NotificationCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt +NotificationTriggerCorrectionsAssigned: Mir wurden Abgaben zur Korrektur zugeteilt CorrCreate: Abgaben erstellen UnknownPseudonymWord pseudonymWord@Text: Unbekanntes Pseudonym-Wort "#{pseudonymWord}" @@ -440,4 +440,18 @@ MessageWarning: Warnung MessageInfo: Information MessageSuccess: Erfolg -InvalidLangFormat: Ungültiger Sprach-Code (RFC1766) \ No newline at end of file +InvalidLangFormat: Ungültiger Sprach-Code (RFC1766) + +ErrorResponseTitleNotFound: Ressource nicht gefunden +ErrorResponseTitleInternalError internalError@Text: Ein interner Fehler ist aufgetreten +ErrorResponseTitleInvalidArgs invalidArgs@Texts: Anfrage-Nachricht enthielt ungültige Argumente +ErrorResponseTitleNotAuthenticated: Anfrage benötigt Authentifizierung +ErrorResponseTitlePermissionDenied permissionDenied@Text: Mangelnde Authorisierung +ErrorResponseTitleBadMethod requestMethod@Method: HTTP-Methode nicht unterstützt + +UnknownErrorResponse: Ein nicht weiter klassifizierter Fehler ist aufgetreten: +ErrorResponseNotFound: Unter der von Ihrem Browser angefragten URL wurde keine Seite gefunden. +ErrorResponseNotAuthenticated: Um Zugriff auf einige Teile des Systems zu erhalten müssen Sie sich zunächst anmelden. +ErrorResponseBadMethod requestMethodText@Text: Ihr Browser kann auf mehrere verschiedene Arten versuchen mit den vom System angebotenen Ressourcen zu interagieren. Die aktuell versuchte Methode (#{requestMethodText}) wird nicht unterstützt. + +ErrorResponseEncrypted: Um keine sensiblen Daten preiszugeben wurden nähere Details verschlüsselt. Wenn Sie eine Anfrage an den Support schicken fügen Sie bitte die unten aufgeführten verschlüsselten Daten mit an. \ No newline at end of file diff --git a/models b/models index 1398a65a5..88e88243f 100644 --- a/models +++ b/models @@ -255,4 +255,8 @@ SystemMessageTranslation language Lang content Html summary Html Maybe - UniqueSystemMessageTranslation message language \ No newline at end of file + UniqueSystemMessageTranslation message language +ClusterConfig + setting ClusterSettingsKey + value Value + Primary setting \ No newline at end of file diff --git a/package.yaml b/package.yaml index 4a48ee43d..44695edb0 100644 --- a/package.yaml +++ b/package.yaml @@ -48,6 +48,7 @@ dependencies: - wai - cryptonite - cryptonite-conduit +- saltine - base64-bytestring - memory - http-api-data @@ -67,6 +68,7 @@ dependencies: - cryptoids - cryptoids-class - binary +- cereal - mtl - sandi - esqueleto @@ -107,6 +109,7 @@ dependencies: - postgresql-simple - word24 - mmorph +- clientsession # The library contains all of our application code. The executable # defined below is just a thin wrapper. diff --git a/src/Application.hs b/src/Application.hs index 9ffcf2106..f52c180e9 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -6,6 +6,9 @@ {-# LANGUAGE ViewPatterns #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE MultiWayIf #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Application ( getApplicationDev, getAppDevSettings @@ -26,7 +29,7 @@ module Application import Control.Monad.Logger (liftLoc, LoggingT(..), MonadLoggerIO(..)) import Database.Persist.Postgresql (createPostgresqlPool, pgConnStr, pgPoolSize, runSqlPool) -import Import +import Import hiding (Proxy) import Language.Haskell.TH.Syntax (qLocation) import Network.Wai (Middleware) import Network.Wai.Handler.Warp (Settings, defaultSettings, @@ -67,6 +70,12 @@ import qualified Yesod.Core.Types as Yesod (Logger(..)) import qualified Data.HashMap.Strict as HashMap import Control.Lens ((&)) + +import Data.Proxy + +import qualified Data.Aeson as Aeson + +import System.Exit (exitFailure) -- Import all relevant handler modules here. -- (HPack takes care to add new modules to our cabal file nowadays.) @@ -105,8 +114,7 @@ makeFoundation appSettings@(AppSettings{..}) = do return $ Yesod.Logger loggerSet tgetter appStatic <- liftIO $ bool static staticDevel appMutableStatic appStaticDir - appCryptoIDKey <- readKeyFile appCryptoIDKeyFile - appInstanceID <- liftIO $ maybe UUID.nextRandom readInstanceIDFile appInstanceIDFile + appInstanceID <- liftIO $ maybe UUID.nextRandom (either readInstanceIDFile return) appInitialInstanceID (appJobCtl, recvChans) <- fmap unzip . atomically . replicateM appJobWorkers $ do chan <- newBroadcastTMChan @@ -120,11 +128,16 @@ makeFoundation appSettings@(AppSettings{..}) = do -- logging function. To get out of this loop, we initially create a -- temporary foundation without a real connection pool, get a log function -- from there, and then create the real foundation. - let mkFoundation appConnPool appSmtpPool = UniWorX {..} + let mkFoundation appConnPool appSmtpPool appCryptoIDKey appSessionKey appErrorMsgKey = UniWorX {..} -- The UniWorX {..} syntax is an example of record wild cards. For more -- information, see: -- https://ocharles.org.uk/blog/posts/2014-12-04-record-wildcards.html - tempFoundation = mkFoundation (error "connPool forced in tempFoundation") (error "smtpPool forced in tempFoundation") + tempFoundation = mkFoundation + (error "connPool forced in tempFoundation") + (error "smtpPool forced in tempFoundation") + (error "cryptoIDKey forced in tempFoundation") + (error "sessionKey forced in tempFoundation") + (error "errorMsgKey forced in tempFoundation") logFunc = messageLoggerSource tempFoundation appLogger flip runLoggingT logFunc $ do @@ -140,12 +153,38 @@ makeFoundation appSettings@(AppSettings{..}) = do -- Perform database migration using our application's logging settings. migrateAll `runSqlPool` sqlPool + appCryptoIDKey <- clusterSetting (Proxy :: Proxy 'ClusterCryptoIDKey) `runSqlPool` sqlPool + appSessionKey <- clusterSetting (Proxy :: Proxy 'ClusterClientSessionKey) `runSqlPool` sqlPool + appErrorMsgKey <- if + | appEncryptErrors -> Just <$> clusterSetting (Proxy :: Proxy 'ClusterErrorMessageKey) `runSqlPool` sqlPool + | otherwise -> return Nothing - handleJobs recvChans $ mkFoundation sqlPool smtpPool + let foundation = mkFoundation sqlPool smtpPool appCryptoIDKey appSessionKey appErrorMsgKey + + handleJobs recvChans foundation -- Return the foundation - return $ mkFoundation sqlPool smtpPool + return foundation +clusterSetting :: forall key m p. + ( MonadIO m + , ClusterSetting key + , MonadLogger m + ) + => p (key :: ClusterSettingsKey) + -> ReaderT SqlBackend m (ClusterSettingValue key) +clusterSetting proxy@(knownClusterSetting -> key) = do + current' <- get (ClusterConfigKey key) + case Aeson.fromJSON . clusterConfigValue <$> current' of + Just (Aeson.Success c) -> return c + Just (Aeson.Error str) -> do + $logErrorS "clusterSetting" $ "Could not parse JSON-Value for " <> toPathPiece key + liftIO exitFailure + Nothing -> do + new <- initClusterSetting proxy + void . insert $ ClusterConfig key (Aeson.toJSON new) + return new + readInstanceIDFile :: MonadIO m => FilePath -> m UUID readInstanceIDFile idFile = liftIO . handle generateInstead $ LBS.readFile idFile >>= parseBS where diff --git a/src/Foundation.hs b/src/Foundation.hs index fe478f1ca..2634974fb 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -20,6 +20,8 @@ import Database.Persist.Sql (ConnectionPool, runSqlPool) import Text.Hamlet (hamletFile) import Text.Jasmine (minifym) +import qualified Web.ClientSession as ClientSession + import Yesod.Auth.Message import Yesod.Auth.Dummy import Auth.LDAP @@ -96,6 +98,10 @@ import qualified Yesod.Auth.Message as Auth import qualified Data.Conduit.List as C +import qualified Crypto.Saltine.Core.SecretBox as SecretBox +import qualified Crypto.Saltine.Class as Saltine +import qualified Data.Binary as Binary + instance DisplayAble b => DisplayAble (E.CryptoID a b) where display = display . ciphertext @@ -127,6 +133,8 @@ data UniWorX = UniWorX , appCryptoIDKey :: CryptoIDKey , appInstanceID :: InstanceId , appJobCtl :: [TMChan JobCtl] + , appErrorMsgKey :: Maybe SecretBox.Key + , appSessionKey :: ClientSession.Key } type SMTPPool = Pool SMTPConnection @@ -197,14 +205,8 @@ instance RenderMessage UniWorX TermIdentifier where Winter -> renderMessage' $ MsgWinterTerm year where renderMessage' = renderMessage foundation ls -instance RenderMessage UniWorX StudyFieldType where - renderMessage foundation ls = renderMessage foundation ls . \case - FieldPrimary -> MsgFieldPrimary - FieldSecondary -> MsgFieldSecondary - newtype ShortTermIdentifier = ShortTermIdentifier TermIdentifier deriving (Eq, Ord, Read, Show) - instance RenderMessage UniWorX ShortTermIdentifier where renderMessage foundation ls (ShortTermIdentifier TermIdentifier{..}) = case season of Summer -> renderMessage' $ MsgSummerTermShort year @@ -214,33 +216,12 @@ instance RenderMessage UniWorX ShortTermIdentifier where instance RenderMessage UniWorX String where renderMessage f ls str = renderMessage f ls $ Text.pack str -instance RenderMessage UniWorX SheetFileType where - renderMessage foundation ls = renderMessage foundation ls . \case - SheetExercise -> MsgSheetExercise - SheetHint -> MsgSheetHint - SheetSolution -> MsgSheetSolution - SheetMarking -> MsgSheetMarking - -instance RenderMessage UniWorX CorrectorState where - renderMessage foundation ls = renderMessage foundation ls . \case - CorrectorNormal -> MsgCorrectorNormal - CorrectorMissing -> MsgCorrectorMissing - CorrectorExcused -> MsgCorrectorExcused - - instance RenderMessage UniWorX Load where renderMessage foundation ls = renderMessage foundation ls . \case (Load {byTutorial=Nothing , byProportion=p}) -> MsgCorByProportionOnly p (Load {byTutorial=Just True , byProportion=p}) -> MsgCorByProportionIncludingTutorial p (Load {byTutorial=Just False, byProportion=p}) -> MsgCorByProportionExcludingTutorial p -instance RenderMessage UniWorX SheetType where - renderMessage foundation ls = renderMessage foundation ls . \case - Bonus{..} -> MsgSheetTypeBonus' maxPoints - Normal{..} -> MsgSheetTypeNormal' maxPoints - Pass{..} -> MsgSheetTypePass' maxPoints passingPoints - NotGraded{} -> MsgSheetTypeNotGraded' - newtype MsgLanguage = MsgLanguage Lang deriving (Eq, Ord, Show, Read) instance RenderMessage UniWorX MsgLanguage where @@ -250,24 +231,18 @@ instance RenderMessage UniWorX MsgLanguage where where mr = renderMessage foundation ls -instance RenderMessage UniWorX NotificationTrigger where - renderMessage foundation ls = renderMessage foundation ls . \case - NTSubmissionRatedGraded -> MsgNotificationTriggerSubmissionRatedGraded - NTSubmissionRated -> MsgNotificationTriggerSubmissionRated - NTSheetActive -> MsgNotificationTriggerSheetActive - NTSheetSoonInactive -> MsgNotificationTriggerSheetSoonInactive - NTSheetInactive -> MsgNotificationTriggerSheetInactive - NTCorrectionsAssigned -> MsgNotificationCorrectionsAssigned - instance RenderMessage UniWorX (UnsupportedAuthPredicate (Route UniWorX)) where renderMessage f ls (UnsupportedAuthPredicate tag route) = renderMessage f ls $ MsgUnsupportedAuthPredicate tag (show route) -instance RenderMessage UniWorX MessageClass where - renderMessage f ls = renderMessage f ls . \case - Error -> MsgMessageError - Warning -> MsgMessageWarning - Info -> MsgMessageInfo - Success -> MsgMessageSuccess +embedRenderMessage ''UniWorX ''MessageClass ("Message" <>) +embedRenderMessage ''UniWorX ''NotificationTrigger $ ("NotificationTrigger" <>) . concat . drop 1 . splitCamel +embedRenderMessage ''UniWorX ''SheetType $ \st -> "SheetType" <> st <> "'" +embedRenderMessage ''UniWorX ''StudyFieldType id +embedRenderMessage ''UniWorX ''SheetFileType id +embedRenderMessage ''UniWorX ''CorrectorState id + +newtype ErrorResponseTitle = ErrorResponseTitle ErrorResponse +embedRenderMessageVariant ''UniWorX ''ErrorResponseTitle ("ErrorResponseTitle" <>) data instance ButtonCssClass UniWorX = BCDefault | BCPrimary | BCSuccess | BCInfo | BCWarning | BCDanger | BCLink @@ -573,9 +548,9 @@ instance Yesod UniWorX where -- Store session data on the client in encrypted cookies, -- default session idle timeout is 120 minutes - makeSessionBackend _ = Just <$> defaultClientSessionBackend - 120 -- timeout in minutes - "client_session_key.aes" + makeSessionBackend UniWorX{appSessionKey,appSettings=AppSettings{appSessionTimeout}} = do + (getCachedDate, _) <- clientSessionDateCacher appSessionTimeout + return . Just $ clientSessionBackend appSessionKey getCachedDate maximumContentLength _ _ = Just $ 50 * 2^20 @@ -627,101 +602,49 @@ instance Yesod UniWorX where $logDebugS "normalizeRouteMiddleware" [st|Redirecting to #{tshow route'}|] redirectWith movedPermanently301 route' - defaultLayout widget = do - master <- getYesod - let AppSettings { appUserDefaults = UserDefaultConf{..}, .. } = appSettings master + -- Since we implement `errorHandler` ourselves we don't need `defaultMessageWidget` + defaultMessageWidget _title _body = error "defaultMessageWidget: undefined" - applySystemMessages - mmsgs <- getMessages + errorHandler err = do + mr <- getMessageRender + let + encrypted :: ToJSON a => a -> Widget -> Widget + encrypted plaintextJson plaintext = do + errKey <- getsYesod appErrorMsgKey + case errKey of + Nothing -> plaintext + Just key -> do + nonce <- liftIO SecretBox.newNonce + let ciphertext = SecretBox.secretbox key nonce . Lazy.ByteString.toStrict $ encode plaintextJson + encoded = decodeUtf8 . Base64.encode . Lazy.ByteString.toStrict $ Binary.encode (Saltine.encode nonce, ciphertext) + formatted = Text.intercalate "\n" . map (Text.intercalate " " . Text.chunksOf 4) $ Text.chunksOf 72 encoded + [whamlet| +

_{MsgErrorResponseEncrypted} +

+                  #{formatted}
+              |]
+        
+        errPage = case err of
+          NotFound -> [whamlet|

_{MsgErrorResponseNotFound}|] + InternalError err -> encrypted err [whamlet|

#{err}|] + InvalidArgs errs -> [whamlet| +