From 3945c35b16c2bd0cb0ac97900f10ab37c4ee30ef Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 6 Apr 2021 20:12:14 -0500 Subject: [PATCH] Add a request.spec GUC for db-root-spec The request.spec GUC contains the schema cache structure in json. It's only available when the root endpoint(/) is requested and when db-root-spec is not empty. --- src/PostgREST/ApiRequest.hs | 10 +++++----- src/PostgREST/App.hs | 2 +- src/PostgREST/Middleware.hs | 25 ++++++++++++++----------- src/PostgREST/QueryBuilder.hs | 4 ++-- test/fixtures/schema.sql | 5 +++++ 5 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/PostgREST/ApiRequest.hs b/src/PostgREST/ApiRequest.hs index e015b195ca0..76f99ba28f2 100644 --- a/src/PostgREST/ApiRequest.hs +++ b/src/PostgREST/ApiRequest.hs @@ -40,7 +40,7 @@ import Network.HTTP.Types.URI (parseQueryReplacePlus, parseSimpleQuery) import Network.Wai (Request (..)) import Network.Wai.Parse (parseHttpAccept) -import Web.Cookie (parseCookiesText) +import Web.Cookie (parseCookies) import Data.Ranged.Boundaries @@ -122,8 +122,8 @@ data ApiRequest = ApiRequest { , iOrder :: [(Text, Text)] -- ^ &order parameters for each level , iCanonicalQS :: ByteString -- ^ Alphabetized (canonical) request query string for response URLs , iJWT :: Text -- ^ JSON Web Token - , iHeaders :: [(Text, Text)] -- ^ HTTP request headers - , iCookies :: [(Text, Text)] -- ^ Request Cookies + , iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers + , iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies , iPath :: ByteString -- ^ Raw request path , iMethod :: ByteString -- ^ Raw request method , iProfile :: Maybe Schema -- ^ The request profile for enabling use of multiple schemas. Follows the spec in hhttps://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/. @@ -170,8 +170,8 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody . map (join (***) toS . second (fromMaybe BS.empty)) $ qString , iJWT = tokenStr - , iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hCookie] - , iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie" + , iHeaders = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie] + , iCookies = maybe [] parseCookies $ lookupHeader "Cookie" , iPath = rawPathInfo req , iMethod = method , iProfile = profile diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 1ab873bd883..bdb9e666c37 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -131,7 +131,7 @@ postgrestResponse conf@AppConfig{..} maybeDbStructure pool time req = do runDbHandler pool (txMode apiRequest) jwtClaims . Middleware.optionalRollback conf apiRequest $ - Middleware.runPgLocals conf jwtClaims handleReq apiRequest + Middleware.runPgLocals conf jwtClaims handleReq apiRequest dbStructure runDbHandler :: SQL.Pool -> SQL.Mode -> Auth.JWTClaims -> DbHandler a -> Handler IO a runDbHandler pool mode jwtClaims handler = do diff --git a/src/PostgREST/Middleware.hs b/src/PostgREST/Middleware.hs index f5683837f53..375c2d10426 100644 --- a/src/PostgREST/Middleware.hs +++ b/src/PostgREST/Middleware.hs @@ -43,11 +43,11 @@ import Network.Wai.Middleware.Static (only, staticPolicy) import qualified PostgREST.Types as Types -import PostgREST.ApiRequest (ApiRequest (..)) +import PostgREST.ApiRequest (ApiRequest (..), Target (..)) import PostgREST.Config (AppConfig (..)) import PostgREST.Error (Error, errorResponseFor) import PostgREST.QueryBuilder (setConfigLocal) -import PostgREST.Types (LogLevel (..), fromQi) +import PostgREST.Types (DbStructure, LogLevel (..), fromQi) import Protolude hiding (head, toS) import Protolude.Conv (toS) import System.IO.Unsafe (unsafePerformIO) @@ -55,28 +55,31 @@ 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 + ApiRequest -> DbStructure -> ExceptT Error H.Transaction Response +runPgLocals conf claims app req dbs = do lift $ H.statement mempty $ H.dynamicallyParameterized - ("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql)) + ("select " <> intercalateSnippet ", " (searchPathSql : methodSql : pathSql : roleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql ++ specSql)) 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) + methodSql = setConfigLocal mempty ("request.method", iMethod req) + pathSql = setConfigLocal mempty ("request.path", 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 + claimsSql = setConfigLocal "request.jwt.claim." <$> [(toS c, toS $ unquoted v) | (c,v) <- M.toList claimsWithRole] + roleSql = maybeToList $ (\x -> setConfigLocal mempty ("role", toS $ unquoted x)) <$> M.lookup "role" claimsWithRole + appSettingsSql = setConfigLocal mempty <$> (join bimap toS <$> configAppSettings conf) searchPathSql = let schemas = T.intercalate ", " (iSchema req : configDbExtraSearchPath conf) in - setConfigLocal mempty ("search_path", schemas) + setConfigLocal mempty ("search_path", toS schemas) preReqSql = (\f -> "select " <> fromQi f <> "();") <$> configDbPreRequest conf + specSql = case iTarget req of + TargetProc{tpIsRootSpec=True} -> [setConfigLocal mempty ("request.spec", toS $ JSON.encode dbs)] + _ -> mempty -- | 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. diff --git a/src/PostgREST/QueryBuilder.hs b/src/PostgREST/QueryBuilder.hs index 13f75c69ba3..75ebb57f766 100644 --- a/src/PostgREST/QueryBuilder.hs +++ b/src/PostgREST/QueryBuilder.hs @@ -174,6 +174,6 @@ limitedQuery :: H.Snippet -> Maybe Integer -> H.Snippet limitedQuery query maxRows = query <> H.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows) -- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL. -setConfigLocal :: Text -> (Text, Text) -> H.Snippet +setConfigLocal :: ByteString -> (ByteString, ByteString) -> H.Snippet setConfigLocal prefix (k, v) = - "set_config(" <> unknownLiteral (prefix <> k) <> ", " <> unknownLiteral v <> ", true)" + "set_config(" <> unknownEncoder (prefix <> k) <> ", " <> unknownEncoder v <> ", true)" diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 637d0a5e7af..9725b6116e3 100644 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1659,6 +1659,11 @@ case current_setting('request.header.accept', true) end $_$ language plpgsql; + +create or replace function root_override() returns json as $$ + select current_setting('request.spec', true)::json; +$$ language sql; + create or replace function welcome() returns text as $$ select 'Welcome to PostgREST'::text; $$ language sql;