* Use ExceptT to avoid 'staircasing' case analysis in App.hs * Split large function in App.hs into individual handler functions * Adapt API of Auth.hs, OpenApi.hs etc. to simplify the use of those modules in App.hs * Split optional rollback functionality into Middleware * Unify SimpleError and ApiRequestError into one Error type, so it can be used across modules
179 lines
7.6 KiB
Haskell
179 lines
7.6 KiB
Haskell
{-|
|
|
Module : PostgREST.Middleware
|
|
Description : Sets CORS policy. Also the PostgreSQL GUCs, role, search_path and pre-request function.
|
|
-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
module PostgREST.Middleware
|
|
( runPgLocals
|
|
, pgrstFormat
|
|
, pgrstMiddleware
|
|
, defaultCorsPolicy
|
|
, corsPolicy
|
|
, optionalRollback
|
|
) where
|
|
|
|
import qualified Hasql.Decoders as HD
|
|
import qualified Hasql.DynamicStatements.Statement as H
|
|
import PostgREST.Private.Common
|
|
|
|
import qualified Data.Aeson as JSON
|
|
import qualified Data.ByteString.Char8 as BS
|
|
import qualified Data.CaseInsensitive as CI
|
|
import Data.Function (id)
|
|
import qualified Data.HashMap.Strict as M
|
|
import Data.List (lookup)
|
|
import Data.Scientific (FPFormat (..),
|
|
formatScientific,
|
|
isInteger)
|
|
import qualified Data.Text as T
|
|
import qualified Hasql.Transaction as H
|
|
import qualified Network.HTTP.Types.Header as HTTP
|
|
import Network.HTTP.Types.Status (Status, status400,
|
|
status500, statusCode)
|
|
import qualified Network.Wai as Wai
|
|
import Network.Wai.Logger (showSockAddr)
|
|
import System.Log.FastLogger (toLogStr)
|
|
|
|
import Network.Wai
|
|
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..),
|
|
cors)
|
|
import Network.Wai.Middleware.Gzip (def, gzip)
|
|
import Network.Wai.Middleware.RequestLogger
|
|
import Network.Wai.Middleware.Static (only, staticPolicy)
|
|
|
|
import qualified PostgREST.Types as Types
|
|
|
|
import PostgREST.ApiRequest (ApiRequest (..))
|
|
import PostgREST.Config (AppConfig (..))
|
|
import PostgREST.Error (Error, errorResponseFor)
|
|
import PostgREST.QueryBuilder (setConfigLocal)
|
|
import PostgREST.Types (LogLevel (..))
|
|
import Protolude hiding (head, toS)
|
|
import Protolude.Conv (toS)
|
|
import System.IO.Unsafe (unsafePerformIO)
|
|
|
|
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
|
runPgLocals :: AppConfig -> M.HashMap Text JSON.Value ->
|
|
(ApiRequest -> ExceptT Error H.Transaction Response) ->
|
|
ApiRequest -> ExceptT Error H.Transaction Response
|
|
runPgLocals conf claims app req = do
|
|
lift $ H.statement mempty $ H.dynamicallyParameterized
|
|
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql))
|
|
HD.noResult (configDbPreparedStatements conf)
|
|
lift $ traverse_ H.sql preReqSql
|
|
app req
|
|
where
|
|
methodSql = setConfigLocal mempty ("request.method", toS $ iMethod req)
|
|
pathSql = setConfigLocal mempty ("request.path", toS $ iPath req)
|
|
headersSql = setConfigLocal "request.header." <$> iHeaders req
|
|
cookiesSql = setConfigLocal "request.cookie." <$> iCookies req
|
|
claimsWithRole =
|
|
let anon = JSON.String . toS $ configDbAnonRole conf in -- role claim defaults to anon if not specified in jwt
|
|
M.union claims (M.singleton "role" anon)
|
|
claimsSql = setConfigLocal "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
|
roleSql = maybeToList $ (\x -> setConfigLocal mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
|
|
appSettingsSql = setConfigLocal mempty <$> configAppSettings conf
|
|
searchPathSql =
|
|
let schemas = T.intercalate ", " (iSchema req : configDbExtraSearchPath conf) in
|
|
setConfigLocal mempty ("search_path", schemas)
|
|
preReqSql = (\f -> "select " <> toS f <> "();") <$> configDbPreRequest conf
|
|
|
|
-- | Log in apache format. Only requests that have a status greater than minStatus are logged.
|
|
-- | There's no way to filter logs in the apache format on wai-extra: https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html#t:OutputFormat.
|
|
-- | So here we copy wai-logger apacheLogStr function: https://github.com/kazu-yamamoto/logger/blob/a4f51b909a099c51af7a3f75cf16e19a06f9e257/wai-logger/Network/Wai/Logger/Apache.hs#L45
|
|
-- | TODO: Add the ability to filter apache logs on wai-extra and remove this function.
|
|
pgrstFormat :: Status -> OutputFormatter
|
|
pgrstFormat minStatus date req status responseSize =
|
|
if status < minStatus
|
|
then mempty
|
|
else toLogStr (getSourceFromSocket req)
|
|
<> " - - ["
|
|
<> toLogStr date
|
|
<> "] \""
|
|
<> toLogStr (requestMethod req)
|
|
<> " "
|
|
<> toLogStr (rawPathInfo req <> rawQueryString req)
|
|
<> " "
|
|
<> toLogStr (show (httpVersion req)::Text)
|
|
<> "\" "
|
|
<> toLogStr (show (statusCode status)::Text)
|
|
<> " "
|
|
<> toLogStr (maybe "-" show responseSize::Text)
|
|
<> " \""
|
|
<> toLogStr (fromMaybe mempty $ requestHeaderReferer req)
|
|
<> "\" \""
|
|
<> toLogStr (fromMaybe mempty $ requestHeaderUserAgent req)
|
|
<> "\"\n"
|
|
where
|
|
getSourceFromSocket = BS.pack . showSockAddr . remoteHost
|
|
|
|
pgrstMiddleware :: LogLevel -> Application -> Application
|
|
pgrstMiddleware logLevel =
|
|
logger
|
|
. gzip def
|
|
. cors corsPolicy
|
|
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
|
where
|
|
logger = case logLevel of
|
|
LogCrit -> id
|
|
LogError -> unsafePerformIO $ mkRequestLogger def { outputFormat = CustomOutputFormat $ pgrstFormat status500}
|
|
LogWarn -> unsafePerformIO $ mkRequestLogger def { outputFormat = CustomOutputFormat $ pgrstFormat status400}
|
|
LogInfo -> logStdout
|
|
|
|
defaultCorsPolicy :: CorsResourcePolicy
|
|
defaultCorsPolicy = CorsResourcePolicy Nothing
|
|
["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
|
(Just $ 60*60*24) False False True
|
|
|
|
-- | CORS policy to be used in by Wai Cors middleware
|
|
corsPolicy :: Request -> Maybe CorsResourcePolicy
|
|
corsPolicy req = case lookup "origin" headers of
|
|
Just origin -> Just defaultCorsPolicy {
|
|
corsOrigins = Just ([origin], True)
|
|
, corsRequestHeaders = "Authentication":accHeaders
|
|
, corsExposedHeaders = Just [
|
|
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
|
|
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
|
|
]
|
|
}
|
|
Nothing -> Nothing
|
|
where
|
|
headers = requestHeaders req
|
|
accHeaders = case lookup "access-control-request-headers" headers of
|
|
Just hdrs -> map (CI.mk . toS . T.strip . toS) $ BS.split ',' hdrs
|
|
Nothing -> []
|
|
|
|
unquoted :: JSON.Value -> Text
|
|
unquoted (JSON.String t) = t
|
|
unquoted (JSON.Number n) =
|
|
toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
|
|
unquoted (JSON.Bool b) = show b
|
|
unquoted v = toS $ JSON.encode v
|
|
|
|
-- | Set a transaction to eventually roll back if requested and set respective
|
|
-- headers on the response.
|
|
optionalRollback
|
|
:: AppConfig
|
|
-> ApiRequest
|
|
-> ExceptT Error H.Transaction Wai.Response
|
|
-> ExceptT Error H.Transaction Wai.Response
|
|
optionalRollback AppConfig{..} ApiRequest{..} transaction = do
|
|
resp <- catchError transaction $ return . errorResponseFor
|
|
when (shouldRollback || (configDbTxRollbackAll && not shouldCommit))
|
|
(lift H.condemn)
|
|
return $ Wai.mapResponseHeaders preferenceApplied resp
|
|
where
|
|
shouldCommit =
|
|
configDbTxAllowOverride && iPreferTransaction == Just Types.Commit
|
|
shouldRollback =
|
|
configDbTxAllowOverride && iPreferTransaction == Just Types.Rollback
|
|
preferenceApplied
|
|
| shouldCommit =
|
|
Types.addHeadersIfNotIncluded
|
|
[(HTTP.hPreferenceApplied, BS.pack (show Types.Commit))]
|
|
| shouldRollback =
|
|
Types.addHeadersIfNotIncluded
|
|
[(HTTP.hPreferenceApplied, BS.pack (show Types.Rollback))]
|
|
| otherwise =
|
|
identity
|