From 627c3c34b70f960249dd520673baa94923b99e01 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Tue, 12 Oct 2021 12:58:52 -0500 Subject: [PATCH] feat: Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14 Getting the value for a header GUC on PostgreSQL v14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims` PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default) --- .github/workflows/ci.yaml | 5 +- CHANGELOG.md | 3 + default.nix | 2 + nix/overlays/default.nix | 1 + nix/overlays/postgresql-default.nix | 2 +- nix/overlays/postgresql-future.nix | 17 +++++ postgrest.cabal | 1 + src/PostgREST/App.hs | 2 +- src/PostgREST/CLI.hs | 4 ++ src/PostgREST/Config.hs | 3 + src/PostgREST/Config/PgVersion.hs | 4 ++ src/PostgREST/Middleware.hs | 40 ++++++++--- test/Feature/LegacyGucsSpec.hs | 68 +++++++++++++++++++ test/Feature/RpcSpec.hs | 44 ++++++++++-- test/Main.hs | 7 ++ test/SpecHelper.hs | 4 ++ test/fixtures/schema.sql | 67 ++++++++++++++---- test/io-tests/configs/expected/aliases.config | 1 + .../configs/expected/boolean-numeric.config | 1 + .../configs/expected/boolean-string.config | 1 + .../io-tests/configs/expected/defaults.config | 1 + ...efaults-with-db-other-authenticator.config | 1 + .../expected/no-defaults-with-db.config | 1 + .../configs/expected/no-defaults.config | 1 + test/io-tests/configs/expected/types.config | 1 + test/io-tests/configs/no-defaults-env.yaml | 1 + test/io-tests/configs/no-defaults.config | 1 + 27 files changed, 250 insertions(+), 34 deletions(-) create mode 100644 nix/overlays/postgresql-future.nix create mode 100644 test/Feature/LegacyGucsSpec.hs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e8209b5ec..e4454b867 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -41,13 +41,16 @@ jobs: - name: Install testing scripts run: nix-env -f default.nix -iA tests withTools - - name: Run coverage (IO tests and Spec tests against PostgreSQL 13) + - name: Run coverage (IO tests and Spec tests against PostgreSQL 14) run: postgrest-coverage - name: Upload coverage to codecov uses: codecov/codecov-action@v2 with: files: ./coverage/codecov.json + - name: Run the spec tests against PostgreSQL 13 + if: always() + run: postgrest-with-postgresql-13 postgrest-test-spec - name: Run the spec tests against PostgreSQL 12 if: always() run: postgrest-with-postgresql-12 postgrest-test-spec diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d773a74..b548174ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Changed - #1927, Overloaded Functions: If there's a function "my_func" having a single unnamed json param and other overloaded pairs(with any number of params), PostgREST won't be able to resolve a POST request to "my_func". For solving this, you can name the unnamed json param `my_func(json) -> my_func(prm json)`. + - #1857, Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14 - @laurenceisla, @robertsosinski + + Getting the value for a header GUC on PostgreSQL 14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims` + + PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default) ## [8.0.0] - 2021-07-25 diff --git a/default.nix b/default.nix index c2563b63a..a4bbf62f1 100644 --- a/default.nix +++ b/default.nix @@ -36,6 +36,7 @@ let allOverlays.gitignore allOverlays.postgresql-default allOverlays.postgresql-legacy + allOverlays.postgresql-future (allOverlays.haskell-packages { inherit compiler; }) ]; @@ -45,6 +46,7 @@ let postgresqlVersions = [ + { name = "postgresql-14"; postgresql = pkgs.postgresql_14; } { name = "postgresql-13"; postgresql = pkgs.postgresql_13; } { name = "postgresql-12"; postgresql = pkgs.postgresql_12; } { name = "postgresql-11"; postgresql = pkgs.postgresql_11; } diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index 2e4f8edbd..05480d5e5 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -5,4 +5,5 @@ haskell-packages = import ./haskell-packages.nix; postgresql-default = import ./postgresql-default.nix; postgresql-legacy = import ./postgresql-legacy.nix; + postgresql-future = import ./postgresql-future.nix; } diff --git a/nix/overlays/postgresql-default.nix b/nix/overlays/postgresql-default.nix index eeb62e315..f337bf34b 100644 --- a/nix/overlays/postgresql-default.nix +++ b/nix/overlays/postgresql-default.nix @@ -1,5 +1,5 @@ self: super: # Overlay that sets the default version of PostgreSQL. { - postgresql = super.postgresql_13; + postgresql = super.postgresql_14; } diff --git a/nix/overlays/postgresql-future.nix b/nix/overlays/postgresql-future.nix new file mode 100644 index 000000000..f6d93db7c --- /dev/null +++ b/nix/overlays/postgresql-future.nix @@ -0,0 +1,17 @@ +self: super: +# Overlay that adds future versions of PostgreSQL that are supported by +# PostgREST. +{ + postgresql_14 = + let + rev = "76b1e16c6659ccef7187ca69b287525fea133244"; + tarballHash = "1vsahpcx80k2bgslspb0sa6j4bmhdx77sw6la455drqcrqhdqj6a"; + + pinnedPkgs = + builtins.fetchTarball { + url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz"; + sha256 = tarballHash; + }; + in + (import pinnedPkgs { }).pkgs.postgresql_14; +} diff --git a/postgrest.cabal b/postgrest.cabal index 55711b407..4711fd48d 100644 --- a/postgrest.cabal +++ b/postgrest.cabal @@ -182,6 +182,7 @@ test-suite spec Feature.InsertSpec Feature.IgnorePrivOpenApiSpec Feature.JsonOperatorSpec + Feature.LegacyGucsSpec Feature.MultipleSchemaSpec Feature.NoJwtSpec Feature.NonexistentSchemaSpec diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 69a6bf810..8a5240c57 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -198,7 +198,7 @@ postgrestResponse conf maybeDbStructure jsonDbS pgVer pool time req = do runDbHandler pool (txMode apiRequest) jwtClaims (configDbPreparedStatements conf) . Middleware.optionalRollback conf apiRequest $ - Middleware.runPgLocals conf jwtClaims handleReq apiRequest jsonDbS + Middleware.runPgLocals conf jwtClaims handleReq apiRequest jsonDbS pgVer runDbHandler :: SQL.Pool -> SQL.Mode -> Auth.JWTClaims -> Bool -> DbHandler a -> Handler IO a runDbHandler pool mode jwtClaims prepared handler = do diff --git a/src/PostgREST/CLI.hs b/src/PostgREST/CLI.hs index 64539ea7f..40a2a2c2e 100644 --- a/src/PostgREST/CLI.hs +++ b/src/PostgREST/CLI.hs @@ -166,6 +166,10 @@ exampleConfigFile = |## Enable in-database configuration |db-config = true | + |## Determine if GUC request settings for headers, cookies and jwt claims use the legacy names (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the new names (string without dashes, valid on all PostgreSQL versions) with json values. + |## For PostgreSQL v14 and up, this setting will be ignored. + |db-use-legacy-gucs = true + | |## how to terminate database transactions |## possible values are: |## commit (default) diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index a0363e5c0..a852e53b0 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -81,6 +81,7 @@ data AppConfig = AppConfig , configDbTxRollbackAll :: Bool , configDbUri :: Text , configDbEmbedDefaultJoin :: JoinType + , configDbUseLegacyGucs :: Bool , configFilePath :: Maybe FilePath , configJWKS :: Maybe JWKSet , configJwtAudience :: Maybe StringOrURI @@ -135,6 +136,7 @@ toText conf = ,("db-tx-end", q . showTxEnd) ,("db-uri", q . configDbUri) ,("db-embed-default-join", q . show . configDbEmbedDefaultJoin) + ,("db-use-legacy-gucs", T.toLower . show . configDbUseLegacyGucs) ,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience) ,("jwt-role-claim-key", q . T.intercalate mempty . fmap show . configJwtRoleClaimKey) ,("jwt-secret", q . toS . showJwtSecret) @@ -226,6 +228,7 @@ parser optPath env dbSettings = <*> parseTxEnd "db-tx-end" fst <*> reqString "db-uri" <*> parseEmbedDefaultJoin "db-embed-default-join" + <*> (fromMaybe True <$> optBool "db-use-legacy-gucs") <*> pure optPath <*> pure Nothing <*> parseJwtAudience "jwt-aud" diff --git a/src/PostgREST/Config/PgVersion.hs b/src/PostgREST/Config/PgVersion.hs index 19e0f68b9..1f0e8c660 100644 --- a/src/PostgREST/Config/PgVersion.hs +++ b/src/PostgREST/Config/PgVersion.hs @@ -12,6 +12,7 @@ module PostgREST.Config.PgVersion , pgVersion114 , pgVersion121 , pgVersion130 + , pgVersion140 ) where import qualified Data.Aeson as JSON @@ -58,3 +59,6 @@ pgVersion121 = PgVersion 120001 "12.1" pgVersion130 :: PgVersion pgVersion130 = PgVersion 130000 "13.0" + +pgVersion140 :: PgVersion +pgVersion140 = PgVersion 140000 "14.0" diff --git a/src/PostgREST/Middleware.hs b/src/PostgREST/Middleware.hs index d8b09529e..c92efbf9b 100644 --- a/src/PostgREST/Middleware.hs +++ b/src/PostgREST/Middleware.hs @@ -14,6 +14,7 @@ module PostgREST.Middleware import qualified Data.Aeson as JSON import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy.Char8 as BSL import qualified Data.CaseInsensitive as CI import qualified Data.HashMap.Strict as M import qualified Data.Text as T @@ -30,6 +31,8 @@ import qualified Network.Wai.Middleware.Gzip as Wai import qualified Network.Wai.Middleware.RequestLogger as Wai import qualified Network.Wai.Middleware.Static as Wai +import Control.Arrow ((***)) + import Data.Function (id) import Data.List (lookup) import Data.Scientific (FPFormat (..), formatScientific, @@ -40,6 +43,7 @@ import System.IO.Unsafe (unsafePerformIO) import System.Log.FastLogger (toLogStr) import PostgREST.Config (AppConfig (..), LogLevel (..)) +import PostgREST.Config.PgVersion (PgVersion (..), pgVersion140) import PostgREST.Error (Error, errorResponseFor) import PostgREST.GucHeader (addHeadersIfNotIncluded) import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet, @@ -54,8 +58,8 @@ import Protolude.Conv (toS) -- | 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 Wai.Response) -> - ApiRequest -> ByteString -> ExceptT Error H.Transaction Wai.Response -runPgLocals conf claims app req jsonDbS = do + ApiRequest -> ByteString -> PgVersion -> ExceptT Error H.Transaction Wai.Response +runPgLocals conf claims app req jsonDbS actualPgVersion = do lift $ H.statement mempty $ H.dynamicallyParameterized ("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql ++ specSql)) HD.noResult (configDbPreparedStatements conf) @@ -64,12 +68,18 @@ runPgLocals conf claims app req jsonDbS = do where 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 + 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) 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." <$> [(toS c, toS $ unquoted v) | (c,v) <- M.toList claimsWithRole] + claimsSql = if usesLegacyGucs + then setConfigLocal "request.jwt.claim." <$> [(toS c, toS $ unquoted v) | (c,v) <- M.toList claimsWithRole] + else [setConfigLocal mempty ("request.jwt.claims", BSL.toStrict $ JSON.encode claimsWithRole)] roleSql = maybeToList $ (\x -> setConfigLocal mempty ("role", toS $ unquoted x)) <$> M.lookup "role" claimsWithRole appSettingsSql = setConfigLocal mempty <$> (join bimap toS <$> configAppSettings conf) searchPathSql = @@ -79,10 +89,7 @@ runPgLocals conf claims app req jsonDbS = do specSql = case iTarget req of TargetProc{tpIsRootSpec=True} -> [setConfigLocal mempty ("request.spec", jsonDbS)] _ -> mempty - -- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL. - setConfigLocal :: ByteString -> (ByteString, ByteString) -> H.Snippet - setConfigLocal prefix (k, v) = - "set_config(" <> unknownEncoder (prefix <> k) <> ", " <> unknownEncoder v <> ", true)" + usesLegacyGucs = configDbUseLegacyGucs conf && actualPgVersion < pgVersion140 -- | 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. @@ -181,3 +188,18 @@ optionalRollback AppConfig{..} ApiRequest{..} transaction = do [(HTTP.hPreferenceApplied, BS.pack (show Rollback))] | otherwise = identity + +-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL. +setConfigLocal :: ByteString -> (ByteString, ByteString) -> H.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)] -> [H.Snippet] +setConfigLocalJson prefix keyVals = [setConfigLocal mempty (prefix, gucJsonVal keyVals)] + where + gucJsonVal :: [(ByteString, ByteString)] -> ByteString + gucJsonVal = BSL.toStrict . JSON.encode . M.fromList . arrayByteStringToText + arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)] + arrayByteStringToText keyVal = (toS *** toS) <$> keyVal diff --git a/test/Feature/LegacyGucsSpec.hs b/test/Feature/LegacyGucsSpec.hs new file mode 100644 index 000000000..a6d062097 --- /dev/null +++ b/test/Feature/LegacyGucsSpec.hs @@ -0,0 +1,68 @@ +module Feature.LegacyGucsSpec where + +import Network.Wai (Application) + +import Network.HTTP.Types +import Test.Hspec hiding (pendingWith) +import Test.Hspec.Wai +import Test.Hspec.Wai.JSON + +import Protolude hiding (get) +import SpecHelper + +spec :: SpecWith ((), Application) +spec = + describe "remote procedure call with legacy gucs disabled" $ do + it "custom header is set" $ + request methodPost "/rpc/get_guc_value" [("Custom-Header", "test")] + [json| { "prefix": "request.headers", "name": "custom-header" } |] + `shouldRespondWith` + [json|"test"|] + { matchStatus = 200 + , matchHeaders = [ matchContentTypeJson ] + } + + it "standard header is set" $ + request methodPost "/rpc/get_guc_value" [("Origin", "http://example.com")] + [json| { "prefix": "request.headers", "name": "origin" } |] + `shouldRespondWith` + [json|"http://example.com"|] + { matchStatus = 200 + , matchHeaders = [ matchContentTypeJson ] + } + + it "current role is available as GUC claim" $ + request methodPost "/rpc/get_guc_value" [] + [json| { "prefix": "request.jwt.claims", "name": "role" } |] + `shouldRespondWith` + [json|"postgrest_test_anonymous"|] + { matchStatus = 200 + , matchHeaders = [ matchContentTypeJson ] + } + + it "single cookie ends up as claims" $ + request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")] + [json| {"prefix": "request.cookies", "name":"acookie"} |] + `shouldRespondWith` + [json|"cookievalue"|] + { matchStatus = 200 + , matchHeaders = [] + } + + it "multiple cookies ends up as claims" $ + request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")] + [json| {"prefix": "request.cookies", "name":"secondcookie"} |] + `shouldRespondWith` + [json|"anothervalue"|] + { matchStatus = 200 + , matchHeaders = [] + } + + it "gets the Authorization value" $ + request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"] + [json| {"prefix": "request.headers", "name":"authorization"} |] + `shouldRespondWith` + [json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|] + { matchStatus = 200 + , matchHeaders = [] + } diff --git a/test/Feature/RpcSpec.hs b/test/Feature/RpcSpec.hs index 0ef6a2f29..0e1eeedc7 100644 --- a/test/Feature/RpcSpec.hs +++ b/test/Feature/RpcSpec.hs @@ -15,7 +15,7 @@ import Text.Heredoc import PostgREST.Config.PgVersion (PgVersion, pgVersion100, pgVersion109, pgVersion110, pgVersion112, pgVersion114, - pgVersion96) + pgVersion140, pgVersion96) import Protolude hiding (get) import SpecHelper @@ -798,7 +798,12 @@ spec actualPgVersion = it "custom header is set" $ request methodPost "/rpc/get_guc_value" [("Custom-Header", "test")] - [json| { "name": "request.header.custom-header" } |] + ( + if actualPgVersion >= pgVersion140 then + [json| { "prefix": "request.headers", "name": "custom-header" } |] + else + [json| { "name": "request.header.custom-header" } |] + ) `shouldRespondWith` [json|"test"|] { matchStatus = 200 @@ -807,7 +812,12 @@ spec actualPgVersion = it "standard header is set" $ request methodPost "/rpc/get_guc_value" [("Origin", "http://example.com")] - [json| { "name": "request.header.origin" } |] + ( + if actualPgVersion >= pgVersion140 then + [json| { "prefix": "request.headers", "name": "origin" } |] + else + [json| { "name": "request.header.origin" } |] + ) `shouldRespondWith` [json|"http://example.com"|] { matchStatus = 200 @@ -815,7 +825,12 @@ spec actualPgVersion = } it "current role is available as GUC claim" $ request methodPost "/rpc/get_guc_value" [] - [json| { "name": "request.jwt.claim.role" } |] + ( + if actualPgVersion >= pgVersion140 then + [json| { "prefix": "request.jwt.claims", "name": "role" } |] + else + [json| { "name": "request.jwt.claim.role" } |] + ) `shouldRespondWith` [json|"postgrest_test_anonymous"|] { matchStatus = 200 @@ -823,7 +838,12 @@ spec actualPgVersion = } it "single cookie ends up as claims" $ request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")] - [json| {"name":"request.cookie.acookie"} |] + ( + if actualPgVersion >= pgVersion140 then + [json| {"prefix": "request.cookies", "name":"acookie"} |] + else + [json| {"name":"request.cookie.acookie"} |] + ) `shouldRespondWith` [json|"cookievalue"|] { matchStatus = 200 @@ -831,7 +851,12 @@ spec actualPgVersion = } it "multiple cookies ends up as claims" $ request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")] - [json| {"name":"request.cookie.secondcookie"} |] + ( + if actualPgVersion >= pgVersion140 then + [json| {"prefix": "request.cookies", "name":"secondcookie"} |] + else + [json| {"name":"request.cookie.secondcookie"} |] + ) `shouldRespondWith` [json|"anothervalue"|] { matchStatus = 200 @@ -847,7 +872,12 @@ spec actualPgVersion = } it "gets the Authorization value" $ request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"] - [json| {"name":"request.header.authorization"} |] + ( + if actualPgVersion >= pgVersion140 then + [json| {"prefix": "request.headers", "name":"authorization"} |] + else + [json| {"name":"request.header.authorization"} |] + ) `shouldRespondWith` [json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|] { matchStatus = 200 diff --git a/test/Main.hs b/test/Main.hs index f5f7822aa..7bbc34f83 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -36,6 +36,7 @@ import qualified Feature.HtmlRawOutputSpec import qualified Feature.IgnorePrivOpenApiSpec import qualified Feature.InsertSpec import qualified Feature.JsonOperatorSpec +import qualified Feature.LegacyGucsSpec import qualified Feature.MultipleSchemaSpec import qualified Feature.NoJwtSpec import qualified Feature.NonexistentSchemaSpec @@ -88,6 +89,7 @@ main = do (configDbSchemas config) (configDbExtraSearchPath config) appState <- AppState.initWithPool pool config + AppState.putPgVersion appState actualPgVersion AppState.putDbStructure appState customDbStructure when (isJust $ configDbRootSpec config) $ AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure @@ -108,6 +110,7 @@ main = do responseHeadersApp = app testCfgResponseHeaders disallowRollbackApp = app testCfgDisallowRollback forceRollbackApp = app testCfgForceRollback + testCfgLegacyGucsApp = app testCfgLegacyGucs extraSearchPathApp = appDbs testCfgExtraSearchPath unicodeApp = appDbs testUnicodeCfg @@ -210,6 +213,10 @@ main = do parallel $ before multipleSchemaApp $ describe "Feature.MultipleSchemaSpec" $ Feature.MultipleSchemaSpec.spec actualPgVersion + -- this test runs with db-uses-legacy-gucs = false + parallel $ before testCfgLegacyGucsApp $ + describe "Feature.LegacyGucsSpec" Feature.LegacyGucsSpec.spec + -- this test runs with db-embed-default-join = inner before embedInnerJoinApp $ describe "Feature.EmbedInnerJoinSpecNotDefaultConfig" Feature.EmbedInnerJoinSpec.notDefaultConfig diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index 290efc81a..39ab5a6f2 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -91,6 +91,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in , configDbConfig = False , configDbUri = mempty , configDbEmbedDefaultJoin = JTLeft + , configDbUseLegacyGucs = True , configFilePath = Nothing , configJWKS = parseSecret <$> secret , configJwtAudience = Nothing @@ -190,6 +191,9 @@ testCfgResponseHeaders testDbConn = (testCfg testDbConn) { configDbPreRequest = testMultipleSchemaCfg :: Text -> AppConfig testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["v1", "v2"] } +testCfgLegacyGucs :: Text -> AppConfig +testCfgLegacyGucs testDbConn = (testCfg testDbConn) { configDbUseLegacyGucs = False } + resetDb :: Text -> IO () resetDb dbConn = loadFixture dbConn "data" diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 3768c64b0..5d36ace7c 100644 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -82,7 +82,10 @@ CREATE FUNCTION set_authors_only_owner() RETURNS trigger LANGUAGE plpgsql AS $$ begin - NEW.owner = current_setting('request.jwt.claim.id'); + NEW.owner = case when current_setting('server_version_num')::int >= 140000 + then current_setting('request.jwt.claims')::json->>'id' + else current_setting('request.jwt.claim.id') + end; RETURN NEW; end $$; @@ -301,7 +304,10 @@ CREATE OR REPLACE FUNCTION switch_role() RETURNS void declare user_id text; Begin - user_id = current_setting('request.jwt.claim.id')::text; + user_id = case when current_setting('server_version_num')::int >= 140000 + then (current_setting('request.jwt.claims')::json->>'id')::text + else current_setting('request.jwt.claim.id')::text + end; if user_id = '1'::text then execute 'set local role postgrest_test_author'; elseif user_id = '2'::text then @@ -329,18 +335,33 @@ CREATE FUNCTION reveal_big_jwt() RETURNS TABLE ( iss text, sub text, exp bigint, nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean ) - LANGUAGE sql SECURITY DEFINER + LANGUAGE plpgsql SECURITY DEFINER STABLE AS $$ -SELECT current_setting('request.jwt.claim.iss') as iss, - current_setting('request.jwt.claim.sub') as sub, - current_setting('request.jwt.claim.exp')::bigint as exp, - current_setting('request.jwt.claim.nbf')::bigint as nbf, - current_setting('request.jwt.claim.iat')::bigint as iat, - current_setting('request.jwt.claim.jti') as jti, - -- role is not included in the claims list - current_setting('request.jwt.claim.http://postgrest.com/foo')::boolean - as "http://postgrest.com/foo"; + BEGIN + -- JWT claims are set in JSON format since v14 + IF (current_setting('server_version_num')::INT >= 140000) THEN + RETURN QUERY + SELECT current_setting('request.jwt.claims')::json->>'iss' as iss, + current_setting('request.jwt.claims')::json->>'sub' as sub, + (current_setting('request.jwt.claims')::json->>'exp')::bigint as exp, + (current_setting('request.jwt.claims')::json->>'nbf')::bigint as nbf, + (current_setting('request.jwt.claims')::json->>'iat')::bigint as iat, + current_setting('request.jwt.claims')::json->>'jti' as jti, + (current_setting('request.jwt.claims')::json->>'http://postgrest.com/foo')::boolean + as "http://postgrest.com/foo"; + ELSE + RETURN QUERY + SELECT current_setting('request.jwt.claim.iss') as iss, + current_setting('request.jwt.claim.sub') as sub, + current_setting('request.jwt.claim.exp')::bigint as exp, + current_setting('request.jwt.claim.nbf')::bigint as nbf, + current_setting('request.jwt.claim.iat')::bigint as iat, + current_setting('request.jwt.claim.jti') as jti, + current_setting('request.jwt.claim.http://postgrest.com/foo')::boolean + as "http://postgrest.com/foo"; + END IF; +END; $$; @@ -1093,6 +1114,11 @@ create function test.get_guc_value(name text) returns text as $$ select nullif(current_setting(name), '')::text; $$ language sql; +-- Get the GUC values for Postgres v14.0 and up +create function test.get_guc_value(prefix text, name text) returns text as $$ +select nullif(current_setting(prefix)::json->>name, '')::text; +$$ language sql; + create table w_or_wo_comma_names ( name text ); create table items_with_different_col_types ( @@ -1784,8 +1810,13 @@ openapi json = $$ } } $$; +accept text; begin -case current_setting('request.header.accept', true) +accept = case when current_setting('server_version_num')::int >= 140000 + then current_setting('request.headers', true)::json->>'accept' + else current_setting('request.header.accept', true) + end; +case accept when 'application/openapi+json' then return openapi; when 'application/json' then @@ -1958,9 +1989,15 @@ add constraint snd_shift foreign key (snd_shift_activity_id, snd_shift -- for a pre-request function create or replace function custom_headers() returns void as $$ declare - user_agent text := current_setting('request.header.user-agent', true); + user_agent text := case when current_setting('server_version_num')::int >= 140000 + then current_setting('request.headers', true)::json->>'user-agent' + else current_setting('request.header.user-agent', true) + end; req_path text := current_setting('request.path', true); - req_accept text := current_setting('request.header.accept', true); + req_accept text := case when current_setting('server_version_num')::int >= 140000 + then current_setting('request.headers', true)::json->>'accept' + else current_setting('request.header.accept', true) + end; req_method text := current_setting('request.method', true); begin if user_agent similar to 'MSIE (6.0|7.0)' then diff --git a/test/io-tests/configs/expected/aliases.config b/test/io-tests/configs/expected/aliases.config index 5630c7756..59ab960aa 100644 --- a/test/io-tests/configs/expected/aliases.config +++ b/test/io-tests/configs/expected/aliases.config @@ -13,6 +13,7 @@ db-config = "false" db-tx-end = "commit" db-uri = "required" db-embed-default-join = "left" +db-use-legacy-gucs = true jwt-aud = "" jwt-role-claim-key = ".\"aliased\"" jwt-secret = "" diff --git a/test/io-tests/configs/expected/boolean-numeric.config b/test/io-tests/configs/expected/boolean-numeric.config index a87effadc..da5c9e700 100644 --- a/test/io-tests/configs/expected/boolean-numeric.config +++ b/test/io-tests/configs/expected/boolean-numeric.config @@ -13,6 +13,7 @@ db-config = "false" db-tx-end = "commit" db-uri = "required" db-embed-default-join = "left" +db-use-legacy-gucs = true jwt-aud = "" jwt-role-claim-key = ".\"role\"" jwt-secret = "" diff --git a/test/io-tests/configs/expected/boolean-string.config b/test/io-tests/configs/expected/boolean-string.config index a87effadc..da5c9e700 100644 --- a/test/io-tests/configs/expected/boolean-string.config +++ b/test/io-tests/configs/expected/boolean-string.config @@ -13,6 +13,7 @@ db-config = "false" db-tx-end = "commit" db-uri = "required" db-embed-default-join = "left" +db-use-legacy-gucs = true jwt-aud = "" jwt-role-claim-key = ".\"role\"" jwt-secret = "" diff --git a/test/io-tests/configs/expected/defaults.config b/test/io-tests/configs/expected/defaults.config index 26ce87c04..25508c7b3 100644 --- a/test/io-tests/configs/expected/defaults.config +++ b/test/io-tests/configs/expected/defaults.config @@ -13,6 +13,7 @@ db-config = "false" db-tx-end = "commit" db-uri = "required" db-embed-default-join = "left" +db-use-legacy-gucs = true jwt-aud = "" jwt-role-claim-key = ".\"role\"" jwt-secret = "" diff --git a/test/io-tests/configs/expected/no-defaults-with-db-other-authenticator.config b/test/io-tests/configs/expected/no-defaults-with-db-other-authenticator.config index 2ebbc0078..f26c0cdb0 100644 --- a/test/io-tests/configs/expected/no-defaults-with-db-other-authenticator.config +++ b/test/io-tests/configs/expected/no-defaults-with-db-other-authenticator.config @@ -13,6 +13,7 @@ db-config = "true" db-tx-end = "rollback-allow-override" db-uri = "" db-embed-default-join = "inner" +db-use-legacy-gucs = false jwt-aud = "https://otherexample.org" jwt-role-claim-key = ".\"other\".\"role\"" jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE" diff --git a/test/io-tests/configs/expected/no-defaults-with-db.config b/test/io-tests/configs/expected/no-defaults-with-db.config index cf112f4ec..90814363b 100644 --- a/test/io-tests/configs/expected/no-defaults-with-db.config +++ b/test/io-tests/configs/expected/no-defaults-with-db.config @@ -13,6 +13,7 @@ db-config = "true" db-tx-end = "commit-allow-override" db-uri = "" db-embed-default-join = "inner" +db-use-legacy-gucs = false jwt-aud = "https://example.org" jwt-role-claim-key = ".\"a\".\"role\"" jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE" diff --git a/test/io-tests/configs/expected/no-defaults.config b/test/io-tests/configs/expected/no-defaults.config index 1bdbb1561..fb3bcab96 100644 --- a/test/io-tests/configs/expected/no-defaults.config +++ b/test/io-tests/configs/expected/no-defaults.config @@ -13,6 +13,7 @@ db-config = "false" db-tx-end = "rollback-allow-override" db-uri = "tmp_db" db-embed-default-join = "inner" +db-use-legacy-gucs = false jwt-aud = "https://postgrest.org" jwt-role-claim-key = ".\"user\"[0].\"real-role\"" jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" diff --git a/test/io-tests/configs/expected/types.config b/test/io-tests/configs/expected/types.config index e4be8133a..1d3b6c22b 100644 --- a/test/io-tests/configs/expected/types.config +++ b/test/io-tests/configs/expected/types.config @@ -13,6 +13,7 @@ db-config = "true" db-tx-end = "commit" db-uri = "required" db-embed-default-join = "left" +db-use-legacy-gucs = true jwt-aud = "" jwt-role-claim-key = ".\"role\"" jwt-secret = "" diff --git a/test/io-tests/configs/no-defaults-env.yaml b/test/io-tests/configs/no-defaults-env.yaml index 9cafc5fbd..27bbe2c83 100644 --- a/test/io-tests/configs/no-defaults-env.yaml +++ b/test/io-tests/configs/no-defaults-env.yaml @@ -15,6 +15,7 @@ PGRST_DB_CONFIG: false PGRST_DB_TX_END: rollback-allow-override PGRST_DB_URI: tmp_db PGRST_DB_EMBED_DEFAULT_JOIN: inner +PGRST_DB_USE_LEGACY_GUCS: false PGRST_JWT_AUD: 'https://postgrest.org' PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"' PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5 diff --git a/test/io-tests/configs/no-defaults.config b/test/io-tests/configs/no-defaults.config index 2a83b8ce7..f09deff3a 100644 --- a/test/io-tests/configs/no-defaults.config +++ b/test/io-tests/configs/no-defaults.config @@ -13,6 +13,7 @@ db-config = "false" db-tx-end = "rollback-allow-override" db-uri = "tmp_db" db-embed-default-join = "inner" +db-use-legacy-gucs = false jwt-aud = "https://postgrest.org" jwt-role-claim-key = ".user[0].\"real-role\"" jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"