Fixes regression where the search path did not recognize schemas with uppercase, spaces and other special characters in their names.
123 lines
5.8 KiB
Haskell
123 lines
5.8 KiB
Haskell
{-|
|
|
Module : PostgREST.Middleware
|
|
Description : Sets CORS policy. Also the PostgreSQL GUCs, role, search_path and pre-request function.
|
|
-}
|
|
{-# LANGUAGE BlockArguments #-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
module PostgREST.Middleware
|
|
( runPgLocals
|
|
, optionalRollback
|
|
) where
|
|
|
|
import qualified Data.Aeson as JSON
|
|
import qualified Data.Aeson.Key as K
|
|
import qualified Data.Aeson.KeyMap as KM
|
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
|
import qualified Data.HashMap.Strict as HM
|
|
import qualified Data.Text.Encoding as T
|
|
import qualified Hasql.Decoders as HD
|
|
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
|
|
import qualified Hasql.DynamicStatements.Statement as SQL
|
|
import qualified Hasql.Transaction as SQL
|
|
import qualified Network.Wai as Wai
|
|
|
|
|
|
import Control.Arrow ((***))
|
|
|
|
import Data.Scientific (FPFormat (..), formatScientific, isInteger)
|
|
|
|
import PostgREST.Config (AppConfig (..))
|
|
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion140)
|
|
import PostgREST.Error (Error, errorResponseFor)
|
|
import PostgREST.GucHeader (addHeadersIfNotIncluded)
|
|
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet,
|
|
pgFmtIdentList, unknownEncoder)
|
|
import PostgREST.Request.ApiRequest (ApiRequest (..), Target (..))
|
|
|
|
import PostgREST.Request.Preferences
|
|
|
|
import Protolude
|
|
|
|
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
|
runPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> Text ->
|
|
(ApiRequest -> ExceptT Error SQL.Transaction Wai.Response) ->
|
|
ApiRequest -> ByteString -> PgVersion -> ExceptT Error SQL.Transaction Wai.Response
|
|
runPgLocals conf claims role app req jsonDbS actualPgVersion = do
|
|
lift $ SQL.statement mempty $ SQL.dynamicallyParameterized
|
|
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql ++ specSql))
|
|
HD.noResult (configDbPreparedStatements conf)
|
|
lift $ traverse_ SQL.sql preReqSql
|
|
app req
|
|
where
|
|
methodSql = setConfigLocal mempty ("request.method", iMethod req)
|
|
pathSql = setConfigLocal mempty ("request.path", iPath req)
|
|
headersSql = if usesLegacyGucs
|
|
then setConfigLocal "request.header." <$> iHeaders req
|
|
else setConfigLocalJson "request.headers" (iHeaders req)
|
|
cookiesSql = if usesLegacyGucs
|
|
then setConfigLocal "request.cookie." <$> iCookies req
|
|
else setConfigLocalJson "request.cookies" (iCookies req)
|
|
claimsSql = if usesLegacyGucs
|
|
then setConfigLocal "request.jwt.claim." <$> [(toUtf8 $ K.toText c, toUtf8 $ unquoted v) | (c,v) <- KM.toList claims]
|
|
else [setConfigLocal mempty ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
|
|
roleSql = [setConfigLocal mempty ("role", toUtf8 role)]
|
|
appSettingsSql = setConfigLocal mempty <$> (join bimap toUtf8 <$> configAppSettings conf)
|
|
searchPathSql =
|
|
let schemas = pgFmtIdentList (iSchema req : configDbExtraSearchPath conf) in
|
|
setConfigLocal mempty ("search_path", schemas)
|
|
preReqSql = (\f -> "select " <> fromQi f <> "();") <$> configDbPreRequest conf
|
|
specSql = case iTarget req of
|
|
TargetProc{tpIsRootSpec=True} -> [setConfigLocal mempty ("request.spec", jsonDbS)]
|
|
_ -> mempty
|
|
usesLegacyGucs = configDbUseLegacyGucs conf && actualPgVersion < pgVersion140
|
|
|
|
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 = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
|
|
|
-- | Set a transaction to eventually roll back if requested and set respective
|
|
-- headers on the response.
|
|
optionalRollback
|
|
:: AppConfig
|
|
-> ApiRequest
|
|
-> ExceptT Error SQL.Transaction Wai.Response
|
|
-> ExceptT Error SQL.Transaction Wai.Response
|
|
optionalRollback AppConfig{..} ApiRequest{..} transaction = do
|
|
resp <- catchError transaction $ return . errorResponseFor
|
|
when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ lift do
|
|
SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"
|
|
SQL.condemn
|
|
return $ Wai.mapResponseHeaders preferenceApplied resp
|
|
where
|
|
shouldCommit =
|
|
configDbTxAllowOverride && iPreferTransaction == Just Commit
|
|
shouldRollback =
|
|
configDbTxAllowOverride && iPreferTransaction == Just Rollback
|
|
preferenceApplied
|
|
| shouldCommit =
|
|
addHeadersIfNotIncluded
|
|
[toAppliedHeader Commit]
|
|
| shouldRollback =
|
|
addHeadersIfNotIncluded
|
|
[toAppliedHeader Rollback]
|
|
| otherwise =
|
|
identity
|
|
|
|
-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.
|
|
setConfigLocal :: ByteString -> (ByteString, ByteString) -> SQL.Snippet
|
|
setConfigLocal prefix (k, v) =
|
|
"set_config(" <> unknownEncoder (prefix <> k) <> ", " <> unknownEncoder v <> ", true)"
|
|
|
|
-- | Starting from PostgreSQL v14, some characters are not allowed for config names (mostly affecting headers with "-").
|
|
-- | A JSON format string is used to avoid this problem. See https://github.com/PostgREST/postgrest/issues/1857
|
|
setConfigLocalJson :: ByteString -> [(ByteString, ByteString)] -> [SQL.Snippet]
|
|
setConfigLocalJson prefix keyVals = [setConfigLocal mempty (prefix, gucJsonVal keyVals)]
|
|
where
|
|
gucJsonVal :: [(ByteString, ByteString)] -> ByteString
|
|
gucJsonVal = LBS.toStrict . JSON.encode . HM.fromList . arrayByteStringToText
|
|
arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]
|
|
arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal
|