diff --git a/config/settings.yml b/config/settings.yml index 449757de4..15bfd8526 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -146,7 +146,7 @@ server-sessions: secure-cookies: "_env:SERVER_SESSION_COOKIES_SECURE:true" session-token-expiration: 28807 session-token-encoding: HS256 -session-samesite: strict +session-samesite: lax user-defaults: max-favourites: 12 diff --git a/frontend/src/app.sass b/frontend/src/app.sass index c1f05706c..81f4f803e 100644 --- a/frontend/src/app.sass +++ b/frontend/src/app.sass @@ -1262,3 +1262,12 @@ a.breadcrumbs__home &__label grid-area: label + +code + display: block + box-shadow: inset 0 0 4px 4px var(--color-grey-light) + white-space: pre-wrap + font-family: monospace + overflow-x: auto + tab-size: 2 + padding: 10px diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index d0a0bf301..3c3b045fd 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -1224,6 +1224,7 @@ MenuAllocationUsers: Bewerber MenuAllocationPriorities: Zentrale Dringlichkeiten MenuAllocationCompute: Platzvergabe berechnen MenuAllocationAccept: Platzvergabe akzeptieren +MenuSwagger: OpenAPI 2.0 (Swagger) BreadcrumbSubmissionFile: Datei BreadcrumbSubmissionUserInvite: Einladung zur Abgabe @@ -1294,7 +1295,8 @@ BreadcrumbAllocationPriorities: Zentrale Dringlichkeiten BreadcrumbAllocationCompute: Platzvergabe berechnen BreadcrumbAllocationAccept: Platzvergabe akzeptieren BreadcrumbExternalApis: Externe APIs -BreadcrumbSwagger: API Dokumentation +BreadcrumbApiDocs: API Dokumentation +BreadcrumbSwagger: OpenAPI 2.0 (Swagger) ExternalExamEdit coursen@CourseName examn@ExamName: Bearbeiten: #{coursen}, #{examn} ExternalExamGrades coursen@CourseName examn@ExamName: Prüfungsleistungen: #{coursen}, #{examn} diff --git a/package.yaml b/package.yaml index bd8f9700e..dd3b531a1 100644 --- a/package.yaml +++ b/package.yaml @@ -145,9 +145,12 @@ dependencies: - servant - servant-server - servant-swagger + - servant-docs - swagger2 - haskell-src-meta - network-uri + - insert-ordered-containers + - vault other-extensions: - GeneralizedNewtypeDeriving diff --git a/routes b/routes index af3f485dd..be75be25f 100644 --- a/routes +++ b/routes @@ -67,7 +67,7 @@ /help HelpR GET POST !free -/external-apis ExternalApisR ServantApiExternalApis getServantApiExternalApis +/external-apis ExternalApisR ServantApiExternalApis getServantApi /user ProfileR GET POST !free /user/profile ProfileDataR GET !free @@ -229,6 +229,8 @@ !/#UUID CryptoUUIDDispatchR GET !free -- just redirect -- !/*{CI FilePath} CryptoFileNameDispatchR GET !free -- Disabled until preliminary check for valid cID exists -/swagger.json SwaggerR GET !free +/api ApiDocsR GET !free +/swagger SwaggerR GET !free +/swagger.json SwaggerJsonR GET !free !/*WellKnownFileName WellKnownR GET !free diff --git a/src/Application.hs b/src/Application.hs index 07303e630..77b2301e9 100644 --- a/src/Application.hs +++ b/src/Application.hs @@ -117,9 +117,10 @@ import Handler.Metrics import Handler.ExternalExam import Handler.Participants import Handler.StorageKey +import Handler.ApiDocs import Handler.Swagger -import ServantApi +import ServantApi () -- YesodSubDispatch instances -- This line actually creates our YesodDispatch instance. It is the second half diff --git a/src/CryptoID.hs b/src/CryptoID.hs index 02ec64b11..78bebd386 100644 --- a/src/CryptoID.hs +++ b/src/CryptoID.hs @@ -73,6 +73,7 @@ decCryptoIDs [ ''SubmissionId , ''CourseNewsId , ''CourseEventId , ''TutorialId + , ''ExternalApiId ] -- CryptoIDNamespace (CI FilePath) SubmissionId ~ "Submission" diff --git a/src/Data/CaseInsensitive/Instances.hs b/src/Data/CaseInsensitive/Instances.hs index 6596fe47e..45e9652e8 100644 --- a/src/Data/CaseInsensitive/Instances.hs +++ b/src/Data/CaseInsensitive/Instances.hs @@ -6,7 +6,7 @@ module Data.CaseInsensitive.Instances ( ) where -import ClassyPrelude.Yesod hiding (lift) +import ClassyPrelude.Yesod hiding (lift, Proxy(..)) import Data.CaseInsensitive (CI) import qualified Data.CaseInsensitive as CI @@ -28,6 +28,10 @@ import Web.HttpApiData import qualified Data.Csv as Csv +import qualified Data.Swagger as Swagger + +import Data.Proxy + instance PersistField (CI Text) where toPersistValue ciText = PersistDbSpecific . Text.encodeUtf8 $ CI.original ciText @@ -88,6 +92,8 @@ instance (CI.FoldCase s, PathPiece s) => PathPiece (CI s) where instance ToHttpApiData s => ToHttpApiData (CI s) where toUrlPiece = toUrlPiece . CI.original toEncodedUrlPiece = toEncodedUrlPiece . CI.original + toHeader = toHeader . CI.original + toQueryParam = toQueryParam . CI.original instance (CI.FoldCase s, FromHttpApiData s) => FromHttpApiData (CI s) where parseUrlPiece = fmap CI.mk . parseUrlPiece @@ -101,3 +107,6 @@ instance Csv.ToField s => Csv.ToField (CI s) where instance (CI.FoldCase s, Csv.FromField s) => Csv.FromField (CI s) where parseField = fmap CI.mk . Csv.parseField + +instance Swagger.ToParamSchema s => Swagger.ToParamSchema (CI s) where + toParamSchema _ = Swagger.toParamSchema (Proxy @s) diff --git a/src/Foundation.hs b/src/Foundation.hs index 4d289a5a2..054c1ef50 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -22,7 +22,9 @@ import Auth.LDAP import Auth.PWHash import Auth.Dummy -import qualified Network.Wai as W (pathInfo) +import qualified Network.Wai as W +import qualified Network.HTTP.Types.Header as W +import qualified Network.Wai.Middleware.HttpAuth as W import Yesod.Core.Types (HandlerContents) import qualified Yesod.Core.Unsafe as Unsafe @@ -312,7 +314,7 @@ askBearerUnsafe :: forall m. -- Use `Handler.Utils.Tokens.requireBearerToken` or `Handler.Utils.Tokens.maybeBearerToken` instead askBearerUnsafe = $cachedHere $ do bearer <- maybeMExceptT (unauthorizedI MsgUnauthorizedNoToken) askBearer - catch (decodeBearer bearer) $ \case + catch (liftHandler $ decodeBearer bearer) $ \case BearerTokenExpired -> throwError =<< unauthorizedI MsgUnauthorizedTokenExpired BearerTokenNotStarted -> throwError =<< unauthorizedI MsgUnauthorizedTokenNotStarted other -> do @@ -1452,7 +1454,7 @@ instance Yesod UniWorX where Nothing -> getApprootText guessApproot app req Just root -> root - makeSessionBackend app@UniWorX{ appSettings' = AppSettings{..}, ..} = sameSite $ case appSessionStore of + makeSessionBackend app@UniWorX{ appSettings' = AppSettings{..}, ..} = notForBearer . sameSite $ case appSessionStore of SessionStorageMemcachedSql sqlStore -> mkBackend =<< stateSettings <$> ServerSession.createState sqlStore SessionStorageAcid acidStore @@ -1484,6 +1486,17 @@ instance Yesod UniWorX where = laxSameSiteSessions | otherwise = id + notForBearer :: IO (Maybe SessionBackend) -> IO (Maybe SessionBackend) + notForBearer = fmap $ fmap notForBearer' + where notForBearer' :: SessionBackend -> SessionBackend + notForBearer' (SessionBackend load) + = let load' req + | aHdrs <- mapMaybe (\(h, v) -> v <$ guard (h == W.hAuthorization)) $ W.requestHeaders req + , any (is _Just) $ map W.extractBearerAuth aHdrs + = return (mempty, const $ return []) + | otherwise + = load req + in SessionBackend load' maximumContentLength app _ = app ^. _appMaximumContentLength @@ -1494,8 +1507,12 @@ instance Yesod UniWorX where -- b) Validates that incoming write requests include that token in either a header or POST parameter. -- To add it, chain it together with the defaultMiddleware: yesodMiddleware = defaultYesodMiddleware . defaultCsrfMiddleware -- For details, see the CSRF documentation in the Yesod.Core.Handler module of the yesod-core package. - yesodMiddleware = observeYesodCacheSizeMiddleware . languagesMiddleware appLanguages . headerMessagesMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . defaultCsrfMiddleware . updateFavouritesMiddleware + yesodMiddleware = observeYesodCacheSizeMiddleware . languagesMiddleware appLanguages . headerMessagesMiddleware . securityHeaderMiddleware . defaultYesodMiddleware . normalizeRouteMiddleware . csrfMiddleware . updateFavouritesMiddleware where + securityHeaderMiddleware :: Handler a -> Handler a + securityHeaderMiddleware handler = (*> handler) $ do + addHeader "X-Frame-Options" "sameorigin" + addHeader "X-Content-Type-Options" "nosniff" updateFavouritesMiddleware :: Handler a -> Handler a updateFavouritesMiddleware handler = (*> handler) . runMaybeT $ do route <- MaybeT getCurrentRoute @@ -1528,48 +1545,78 @@ instance Yesod UniWorX where addCustomHeader HeaderAlerts . decodeUtf8 . urlEncode True . toStrict . JSON.encode observeYesodCacheSizeMiddleware :: Handler a -> Handler a observeYesodCacheSizeMiddleware handler = handler `finally` observeYesodCacheSize + csrfMiddleware :: Handler a -> Handler a + csrfMiddleware handler = do + hasBearer <- is _Just <$> lookupBearerAuth + + if | hasBearer -> handler + | otherwise -> defaultCsrfMiddleware handler -- Since we implement `errorHandler` ourselves we don't need `defaultMessageWidget` defaultMessageWidget _title _body = error "defaultMessageWidget: undefined" errorHandler err = do - mr <- getMessageRender - let - encrypted :: ToJSON a => a -> Widget -> Widget - encrypted plaintextJson plaintext = do - canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True - shouldEncrypt <- getsYesod $ view _appEncryptErrors - if - | shouldEncrypt - , not canDecrypt -> do - ciphertext <- encodedSecretBox SecretBoxPretty plaintextJson + shouldEncrypt <- do + canDecrypt <- (== Authorized) <$> evalAccess AdminErrMsgR True + shouldEncrypt <- getsYesod $ view _appEncryptErrors + return $ shouldEncrypt && not canDecrypt - [whamlet| -

_{MsgErrorResponseEncrypted} -

-                    #{ciphertext}
+      selectRep $ do
+        provideRep $ do
+          mr <- getMessageRender
+          let
+            encrypted :: ToJSON a => a -> Widget -> Widget
+            encrypted plaintextJson plaintext = do
+              if
+                | shouldEncrypt -> do
+                    ciphertext <- encodedSecretBox SecretBoxPretty plaintextJson
+
+                    [whamlet|
+                      

_{MsgErrorResponseEncrypted} +

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

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

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