diff --git a/main/Main.hs b/main/Main.hs index b0a0550b0..761e1bbc4 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -8,7 +8,7 @@ import qualified Data.ByteString.Lazy as LBS import qualified Hasql.Connection as C import qualified Hasql.Notifications as N import qualified Hasql.Pool as P -import qualified Hasql.Session as S +import qualified Hasql.Transaction as HT import qualified Hasql.Transaction.Sessions as HT import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, @@ -28,8 +28,7 @@ import Data.Time.Clock (getCurrentTime) import Network.Wai.Handler.Warp (defaultSettings, runSettings, setHost, setPort, setServerName) import System.CPUTime (getCPUTime) -import System.IO (BufferMode (..), hPrint, - hSetBuffering) +import System.IO (BufferMode (..), hSetBuffering) import Text.Printf (hPrintf) import PostgREST.App (postgrest) @@ -37,6 +36,7 @@ import PostgREST.Config import PostgREST.DbStructure (getDbStructure, getPgVersion) import PostgREST.Error (PgError (PgError), checkIsFatal, errorPayload) +import PostgREST.Statements (dbSettingsStatement) import PostgREST.Types (ConnectionStatus (..), DbStructure, PgVersion (..), SCacheStatus (..), minimumPgVersion) @@ -68,7 +68,7 @@ main = do opts <- readCLIShowHelp env -- build the 'AppConfig' from the config file path - conf <- readValidateConfig env $ cliPath opts + conf <- readValidateConfig mempty env $ cliPath opts -- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server. let @@ -88,19 +88,7 @@ main = do poolSize = configDbPoolSize conf poolTimeout = configDbPoolTimeout' conf logLevel = configLogLevel conf - - case cliCommand opts of - CmdDumpConfig -> - do - putStr $ dumpAppConfig conf - exitSuccess - CmdDumpSchema -> - do - dumpedSchema <- dumpSchema conf - putStrLn dumpedSchema - exitSuccess - CmdRun -> - pass + gucConfigEnabled = configDbLoadGucConfig conf -- create connection pool with the provided settings, returns either a 'Connection' or a 'ConnectionError'. Does not throw. pool <- P.acquire (poolSize, poolTimeout, dbUri) @@ -117,6 +105,24 @@ main = do -- Config that can change at runtime refConf <- newIORef conf + -- re-read and override the config if db-load-guc-config is true + when gucConfigEnabled $ + reReadConfig pool gucConfigEnabled env (cliPath opts) refConf + + case cliCommand opts of + CmdDumpConfig -> + do + dumpedConfig <- dumpAppConfig <$> readIORef refConf + putStr dumpedConfig + exitSuccess + CmdDumpSchema -> + do + dumpedSchema <- dumpSchema pool =<< readIORef refConf + putStrLn dumpedSchema + exitSuccess + CmdRun -> + pass + -- This is passed to the connectionWorker method so it can kill the main thread if the PostgreSQL's version is not supported. mainTid <- myThreadId @@ -141,11 +147,10 @@ main = do Catch connWorker ) Nothing - -- Re-read the config on SIGUSR2, but only if we have a config file - when (isJust $ cliPath opts) $ - void $ installHandler sigUSR2 ( - Catch $ reReadConfig env (cliPath opts) refConf - ) Nothing + -- Re-read the config on SIGUSR2 + void $ installHandler sigUSR2 ( + Catch $ reReadConfig pool gucConfigEnabled env (cliPath opts) refConf >> putStrLn ("Config reloaded" :: Text) + ) Nothing #endif -- reload schema cache on NOTIFY @@ -267,6 +272,15 @@ connectionStatus pool = putStrLn $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..." return itShould +loadDbSettings :: P.Pool -> IO [(Text, Text)] +loadDbSettings pool = do + result <- P.use pool $ HT.transaction HT.ReadCommitted HT.Read $ HT.statement mempty dbSettingsStatement + case result of + Left e -> do + hPutStrLn stderr ("An error ocurred when trying to query database settings for the config parameters:\n" <> show e :: Text) + pure [] + Right x -> pure x + -- | Load the DbStructure by using a connection from the pool. loadSchemaCache :: P.Pool -> PgVersion -> IORef AppConfig -> IORef (Maybe DbStructure) -> IO SCacheStatus loadSchemaCache pool actualPgVersion refConf refDbStructure = do @@ -332,41 +346,33 @@ listener dbUri dbChannel pool refConf refDbStructure mvarConnectionStatus connWo errorMessage = "Could not listen for notifications on the " <> dbChannel <> " channel" :: Text retryMessage = "Retrying listening for notifications on the " <> dbChannel <> " channel.." :: Text -#ifndef mingw32_HOST_OS --- | Re-reads the config at runtime. Invoked on SIGUSR2. +-- | Re-reads the config at runtime. -- | If it panics(config path was changed, invalid setting), it'll show an error but won't kill the main thread. -reReadConfig :: Environment -> Maybe FilePath -> IORef AppConfig -> IO () -reReadConfig env path refConf = do - conf <- readValidateConfig env path +reReadConfig :: P.Pool -> Bool -> Environment -> Maybe FilePath -> IORef AppConfig -> IO () +reReadConfig pool gucConfigEnabled env path refConf = do + dbSettings <- if gucConfigEnabled then loadDbSettings pool else pure [] + conf <- readValidateConfig dbSettings env path atomicWriteIORef refConf conf - putStrLn ("Config file reloaded" :: Text) -#endif -- | Dump DbStructure schema to JSON -dumpSchema :: AppConfig -> IO LBS.ByteString -dumpSchema conf = - do - eitherConn <- C.acquire . toS $ configDbUri conf - case eitherConn of - Left e -> hPrint stderr e >> exitFailure - Right conn -> do - result <- - timeToStderr "Loaded schema in %.3f seconds" $ - flip S.run conn $ do - pgVersion <- getPgVersion - HT.transaction HT.ReadCommitted HT.Read $ - getDbStructure - (toList $ configDbSchemas conf) - (configDbExtraSearchPath conf) - pgVersion - (configDbPreparedStatements conf) - C.release conn - case result of - Left e -> do - hPutStrLn stderr $ "An error ocurred when loading the schema cache:\n" <> show e - exitFailure - Right dbStructure -> return $ Aeson.encode dbStructure - +dumpSchema :: P.Pool -> AppConfig -> IO LBS.ByteString +dumpSchema pool conf = do + result <- + timeToStderr "Loaded schema in %.3f seconds" $ + P.use pool $ do + pgVersion <- getPgVersion + HT.transaction HT.ReadCommitted HT.Read $ + getDbStructure + (toList $ configDbSchemas conf) + (configDbExtraSearchPath conf) + pgVersion + (configDbPreparedStatements conf) + P.release pool + case result of + Left e -> do + hPutStrLn stderr $ "An error ocurred when loading the schema cache:\n" <> show e + exitFailure + Right dbStructure -> return $ Aeson.encode dbStructure -- | Print the time taken to run an IO action to stderr with the given printf string timeToStderr :: [Char] -> IO (Either a b) -> IO (Either a b) diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index e04192cfe..43138bee3 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -45,6 +45,7 @@ import Control.Monad (fail) import Crypto.JWT (JWKSet, StringOrURI, stringOrUri) import Data.Aeson (encode, toJSON) import Data.Either.Combinators (fromRight', whenLeft) +import Data.List (lookup) import Data.List.NonEmpty (fromList, toList) import Data.Maybe (fromJust) import Data.Scientific (floatingOrInteger) @@ -101,6 +102,7 @@ data AppConfig = AppConfig { , configDbPreparedStatements :: Bool , configDbRootSpec :: Maybe Text , configDbSchemas :: NonEmpty Text + , configDbLoadGucConfig :: Bool , configDbTxAllowOverride :: Bool , configDbTxRollbackAll :: Bool , configDbUri :: Text @@ -215,6 +217,9 @@ readCLIShowHelp env = customExecParser parserPrefs opts |## Enable or disable the notification channel |db-channel-enabled = false | + |## Enable loading config parameters from the database by changing the connection role settings + |db-load-guc-config = true + | |## how to terminate database transactions |## possible values are: |## commit (default) @@ -280,6 +285,7 @@ dumpAppConfig conf = ,("db-prepared-statements", toLower . show . configDbPreparedStatements) ,("db-root-spec", q . fromMaybe mempty . configDbRootSpec) ,("db-schemas", q . intercalate "," . toList . configDbSchemas) + ,("db-load-guc-config", q . toLower . show . configDbLoadGucConfig) ,("db-tx-end", q . showTxEnd) ,("db-uri", q . configDbUri) ,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience) @@ -313,7 +319,7 @@ dumpAppConfig conf = secret = fromMaybe mempty $ configJwtSecret c showSocketMode c = showOct (fromRight' $ configServerUnixSocketMode c) "" --- This class is needed for the polymorphism of overrideFromEnvironment +-- This class is needed for the polymorphism of overrideFromDbOrEnvironment -- because C.required and C.optional have different signatures class JustIfMaybe a b where justIfMaybe :: a -> b @@ -325,8 +331,8 @@ instance JustIfMaybe a (Maybe a) where justIfMaybe a = Just a -- | Parse the config file -readAppConfig :: Environment -> Maybe FilePath -> IO AppConfig -readAppConfig env optPath = do +readAppConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig +readAppConfig dbSettings env optPath = do -- Now read the actual config file conf <- case optPath of Just cfgPath -> catches (C.load cfgPath) @@ -356,12 +362,13 @@ readAppConfig env optPath = do <*> (fromMaybe 10 <$> optInt "db-pool-timeout") <*> optWithAlias (optString "db-pre-request") (optString "pre-request") - <*> (fromMaybe True <$> optBool "db-prepared-statements") + <*> (fromMaybe True <$> optBool "db-prepared-statements") <*> optWithAlias (optString "db-root-spec") (optString "root-spec") <*> (fromList . splitOnCommas <$> reqWithAlias (optValue "db-schemas") (optValue "db-schema") "missing key: either db-schemas or db-schema must be set") + <*> (fromMaybe True <$> optBool "db-load-guc-config") <*> parseTxEnd "db-tx-end" snd <*> parseTxEnd "db-tx-end" fst <*> reqString "db-uri" @@ -387,21 +394,27 @@ readAppConfig env optPath = do fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env normalize k = ("app.settings." <>) <$> stripPrefix "PGRST_APP_SETTINGS_" (toS k) - overrideFromEnvironment :: JustIfMaybe a b => + overrideFromDbOrEnvironment :: JustIfMaybe a b => (C.Key -> C.Parser C.Value a -> C.Parser C.Config b) -> C.Key -> (C.Value -> a) -> C.Parser C.Config b - overrideFromEnvironment necessity key coercion = - case M.lookup name env of - Just envVal -> pure $ justIfMaybe $ coercion $ C.String envVal - Nothing -> necessity key (coercion <$> C.value) + overrideFromDbOrEnvironment necessity key coercion = + case reloadableDbSetting <|> M.lookup name env of + Just dbOrEnvVal -> pure $ justIfMaybe $ coercion $ C.String dbOrEnvVal + Nothing -> necessity key (coercion <$> C.value) where name = "PGRST_" <> map capitalize (toS key) capitalize '-' = '_' capitalize c = toUpper c + reloadableDbSetting = + if key `notElem` [ + "server-host", "server-port", "server-unix-socket", "server-unix-socket-mode", "log-level", + "db-anon-role", "db-uri", "db-channel-enabled", "db-channel", "db-pool", "db-pool-timeout", "db-load-guc-config"] + then lookup key dbSettings + else Nothing parseSocketFileMode :: C.Key -> C.Parser C.Config (Either Text FileMode) parseSocketFileMode k = - overrideFromEnvironment C.optional k coerceText >>= \case + overrideFromDbOrEnvironment C.optional k coerceText >>= \case Nothing -> pure $ Right 432 -- return default 660 mode if no value was provided Just fileModeText -> case (readOct . unpack) fileModeText of @@ -414,7 +427,7 @@ readAppConfig env optPath = do parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI) parseJwtAudience k = - overrideFromEnvironment C.optional k coerceText >>= \case + overrideFromDbOrEnvironment C.optional k coerceText >>= \case Nothing -> pure Nothing -- no audience in config file Just aud -> case preview stringOrUri (unpack aud) of Nothing -> fail "Invalid Jwt audience. Check your configuration." @@ -423,7 +436,7 @@ readAppConfig env optPath = do parseLogLevel :: C.Key -> C.Parser C.Config LogLevel parseLogLevel k = - overrideFromEnvironment C.optional k coerceText >>= \case + overrideFromDbOrEnvironment C.optional k coerceText >>= \case Nothing -> pure LogError Just "" -> pure LogError Just "crit" -> pure LogCrit @@ -434,7 +447,7 @@ readAppConfig env optPath = do parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool parseTxEnd k f = - overrideFromEnvironment C.optional k coerceText >>= \case + overrideFromDbOrEnvironment C.optional k coerceText >>= \case -- RollbackAll AllowOverride Nothing -> pure $ f (False, False) Just "" -> pure $ f (False, False) @@ -460,19 +473,19 @@ readAppConfig env optPath = do Nothing -> alias reqString :: C.Key -> C.Parser C.Config Text - reqString k = overrideFromEnvironment C.required k coerceText + reqString k = overrideFromDbOrEnvironment C.required k coerceText optString :: C.Key -> C.Parser C.Config (Maybe Text) - optString k = mfilter (/= "") <$> overrideFromEnvironment C.optional k coerceText + optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText optValue :: C.Key -> C.Parser C.Config (Maybe C.Value) - optValue k = overrideFromEnvironment C.optional k identity + optValue k = overrideFromDbOrEnvironment C.optional k identity optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i) - optInt k = join <$> overrideFromEnvironment C.optional k coerceInt + optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt optBool :: C.Key -> C.Parser C.Config (Maybe Bool) - optBool k = join <$> overrideFromEnvironment C.optional k coerceBool + optBool k = join <$> overrideFromDbOrEnvironment C.optional k coerceBool coerceText :: C.Value -> Text coerceText (C.String s) = s @@ -506,10 +519,10 @@ readAppConfig env optPath = do hPutStrLn stderr err exitFailure --- | Parse the AppConfig and validate it. Panic on invalid config options. -readValidateConfig :: Environment -> Maybe FilePath -> IO AppConfig -readValidateConfig env path = do - conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig env path +-- | Parse the AppConfig and validate it. Overrides the config options from env vars or db settings. Panics on invalid config options. +readValidateConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig +readValidateConfig dbSettings env path = do + conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig dbSettings env path -- Checks that the provided proxy uri is formated correctly when (isMalformedProxyUri $ toS <$> configOpenApiServerProxyUri conf) $ panic diff --git a/src/PostgREST/Statements.hs b/src/PostgREST/Statements.hs index 86b2c257f..0cb5a6046 100644 --- a/src/PostgREST/Statements.hs +++ b/src/PostgREST/Statements.hs @@ -14,6 +14,7 @@ module PostgREST.Statements ( , createReadStatement , callProcStatement , createExplainStatement + , dbSettingsStatement ) where @@ -24,6 +25,7 @@ import qualified Data.ByteString.Char8 as BS import Data.Maybe import Data.Text.Read (decimal) import qualified Hasql.Decoders as HD +import qualified Hasql.Encoders as HE import qualified Hasql.Statement as H import Network.HTTP.Types.Status import PostgREST.Error @@ -37,6 +39,8 @@ import Protolude.Conv (toS) import qualified Hasql.DynamicStatements.Snippet as H import qualified Hasql.DynamicStatements.Statement as H +import Text.InterpolatedString.Perl6 (q) + {-| The generic query result format used by API responses. The location header is represented as a list of strings containing variable bindings like @"k1=eq.42"@, or the empty list if there is no location header. @@ -190,3 +194,19 @@ decodeGucHeaders = first (const GucHeadersError) . JSON.eitherDecode . toS <$> H decodeGucStatus :: HD.Value (Either SimpleError (Maybe Status)) decodeGucStatus = first (const GucStatusError) . fmap (Just . toEnum . fst) . decimal <$> HD.text + +-- | Get db settings from the connection role. Only used for configuration. +dbSettingsStatement :: H.Statement () [(Text, Text)] +dbSettingsStatement = H.Statement sql HE.noParams decodeSettings False + where + sql = [q| + with + role_setting as ( + select unnest(setconfig) as setting from pg_catalog.pg_db_role_setting where setrole = 'postgrest_test_authenticator'::regrole::oid + ), + kv_settings as ( + select split_part(setting, '=', 1) as key, split_part(setting, '=', 2) as value from role_setting + ) + select replace(key, 'pgrst.', '') as key, value from kv_settings where key like 'pgrst.%'; + |] + decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs index e6d24f49f..b5475160b 100644 --- a/test/SpecHelper.hs +++ b/test/SpecHelper.hs @@ -78,6 +78,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in , configDbPreparedStatements = True , configDbRootSpec = Nothing , configDbSchemas = fromList ["test"] + , configDbLoadGucConfig = False , configDbUri = mempty , configJWKS = parseSecret <$> secret , configJwtAudience = Nothing diff --git a/test/fixtures/roles.sql b/test/fixtures/roles.sql index 93b962d83..53a9eafb4 100644 --- a/test/fixtures/roles.sql +++ b/test/fixtures/roles.sql @@ -5,3 +5,32 @@ CREATE ROLE postgrest_test_default_role; CREATE ROLE postgrest_test_author; GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO :USER; + +-- reloadable config options for io tests +ALTER ROLE postgrest_test_authenticator SET pgrst."jwt-aud" = 'https://example.org'; +ALTER ROLE postgrest_test_authenticator SET pgrst."openapi-server-proxy-uri" = 'https://example.org/api'; +ALTER ROLE postgrest_test_authenticator SET pgrst."raw-media-types" = 'application/vnd.pgrst.db-config'; +ALTER ROLE postgrest_test_authenticator SET pgrst."jwt-secret" = 'REALLYREALLYREALLYREALLYVERYSAFE'; +ALTER ROLE postgrest_test_authenticator SET pgrst."jwt-secret-is-base64" = 'true'; +ALTER ROLE postgrest_test_authenticator SET pgrst."jwt-role-claim-key" = '."a"."role"'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-tx-end" = 'commit-allow-override'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-schemas" = 'test, tenant1, tenant2'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-root-spec" = 'root'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-prepared-statements" = 'false'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-pre-request" = 'custom_headers'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-max-rows" = '1000'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-extra-search-path" = 'public, extensions'; + +-- non-reloadable configs for io tests +ALTER ROLE postgrest_test_authenticator SET pgrst."server-host" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."server-port" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."server-unix-socket" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."server-unix-socket-mode" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."log-level" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-anon-role" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-uri" = 'postgresql://ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-channel-enabled" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-channel" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-pool" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-pool-timeout" = 'ignored'; +ALTER ROLE postgrest_test_authenticator SET pgrst."db-load-guc-config" = 'ignored'; diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 32f79d51e..7248540b9 100644 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -1924,3 +1924,10 @@ $$ language sql; -- Only used for manually testing creating prepared statements create view prepared_statements as select * from pg_catalog.pg_prepared_statements; + +create or replace function change_max_rows_config(val int) returns void as $_$ +begin + execute format($$ + alter role postgrest_test_authenticator set pgrst."db-max-rows" = %L; + $$, val); +end $_$ volatile security definer language plpgsql ; diff --git a/test/io-tests/configs/aliases.config b/test/io-tests/configs/aliases.config index 6a858f333..b3e1f1e7c 100644 --- a/test/io-tests/configs/aliases.config +++ b/test/io-tests/configs/aliases.config @@ -7,3 +7,4 @@ pre-request = "check_alias" role-claim-key = ".aliased" root-spec = "open_alias" secret-is-base64 = true +db-load-guc-config = false diff --git a/test/io-tests/configs/app-settings.config b/test/io-tests/configs/app-settings.config index 20a811abd..9ff1ca19d 100644 --- a/test/io-tests/configs/app-settings.config +++ b/test/io-tests/configs/app-settings.config @@ -2,3 +2,4 @@ db-pool = 1 db-pool-timeout = 1 app.settings.external_api_secret = "0123456789abcdef" +db-load-guc-config = false diff --git a/test/io-tests/configs/base64-secret-from-file.config b/test/io-tests/configs/base64-secret-from-file.config index 5040da58d..95f421004 100644 --- a/test/io-tests/configs/base64-secret-from-file.config +++ b/test/io-tests/configs/base64-secret-from-file.config @@ -3,3 +3,4 @@ db-pool = 1 # Read secret from a file: /dev/stdin (alias for standard input) jwt-secret = "@/dev/stdin" jwt-secret-is-base64 = true +db-load-guc-config = false diff --git a/test/io-tests/configs/boolean-numeric.config b/test/io-tests/configs/boolean-numeric.config index 6dbe6c88e..550572117 100644 --- a/test/io-tests/configs/boolean-numeric.config +++ b/test/io-tests/configs/boolean-numeric.config @@ -5,3 +5,4 @@ db-anon-role = "required" db-channel-enabled = "1" db-prepared-statements = "0" jwt-secret-is-base64 = "2" +db-load-guc-config = false diff --git a/test/io-tests/configs/boolean-string.config b/test/io-tests/configs/boolean-string.config index f7967cd4f..26f53d495 100644 --- a/test/io-tests/configs/boolean-string.config +++ b/test/io-tests/configs/boolean-string.config @@ -5,3 +5,4 @@ db-anon-role = "required" db-channel-enabled = "true" db-prepared-statements = "FALSE" jwt-secret-is-base64 = "\"true\"" +db-load-guc-config = false diff --git a/test/io-tests/configs/dburi-from-file.config b/test/io-tests/configs/dburi-from-file.config index 2e52488ca..8352d2d1a 100644 --- a/test/io-tests/configs/dburi-from-file.config +++ b/test/io-tests/configs/dburi-from-file.config @@ -1,3 +1,4 @@ db-uri = "@/dev/stdin" db-pool = 1 jwt-secret = "reallyreallyreallyreallyverysafe" +db-load-guc-config = false diff --git a/test/io-tests/configs/defaults.config b/test/io-tests/configs/defaults.config index 7936cc34a..539810bfa 100644 --- a/test/io-tests/configs/defaults.config +++ b/test/io-tests/configs/defaults.config @@ -1,3 +1,5 @@ db-uri = "required" db-schemas = "required" db-anon-role = "required" +# Not the default, but only works with proper db-uri +db-load-guc-config = false diff --git a/test/io-tests/configs/expected/aliases.config b/test/io-tests/configs/expected/aliases.config index ee3747d31..8e82dbd76 100644 --- a/test/io-tests/configs/expected/aliases.config +++ b/test/io-tests/configs/expected/aliases.config @@ -9,6 +9,7 @@ db-pre-request = "check_alias" db-prepared-statements = true db-root-spec = "open_alias" db-schemas = "provided_through_alias" +db-load-guc-config = "false" db-tx-end = "commit" db-uri = "required" jwt-aud = "" diff --git a/test/io-tests/configs/expected/boolean-numeric.config b/test/io-tests/configs/expected/boolean-numeric.config index d5460e7de..dd47e8742 100644 --- a/test/io-tests/configs/expected/boolean-numeric.config +++ b/test/io-tests/configs/expected/boolean-numeric.config @@ -9,6 +9,7 @@ db-pre-request = "" db-prepared-statements = false db-root-spec = "" db-schemas = "required" +db-load-guc-config = "false" db-tx-end = "commit" db-uri = "required" jwt-aud = "" diff --git a/test/io-tests/configs/expected/boolean-string.config b/test/io-tests/configs/expected/boolean-string.config index d5460e7de..dd47e8742 100644 --- a/test/io-tests/configs/expected/boolean-string.config +++ b/test/io-tests/configs/expected/boolean-string.config @@ -9,6 +9,7 @@ db-pre-request = "" db-prepared-statements = false db-root-spec = "" db-schemas = "required" +db-load-guc-config = "false" db-tx-end = "commit" db-uri = "required" jwt-aud = "" diff --git a/test/io-tests/configs/expected/defaults.config b/test/io-tests/configs/expected/defaults.config index 80e5b937e..8f18c2e2e 100644 --- a/test/io-tests/configs/expected/defaults.config +++ b/test/io-tests/configs/expected/defaults.config @@ -9,6 +9,7 @@ db-pre-request = "" db-prepared-statements = true db-root-spec = "" db-schemas = "required" +db-load-guc-config = "false" db-tx-end = "commit" db-uri = "required" jwt-aud = "" diff --git a/test/io-tests/configs/expected/no-defaults-with-db.config b/test/io-tests/configs/expected/no-defaults-with-db.config new file mode 100644 index 000000000..67d268bd9 --- /dev/null +++ b/test/io-tests/configs/expected/no-defaults-with-db.config @@ -0,0 +1,27 @@ +db-anon-role = "postgrest_test_anonymous" +db-channel = "postgrest" +db-channel-enabled = true +db-extra-search-path = "public,extensions" +db-max-rows = 1000 +db-pool = 1 +db-pool-timeout = 100 +db-pre-request = "custom_headers" +db-prepared-statements = false +db-root-spec = "root" +db-schemas = "test,tenant1,tenant2" +db-load-guc-config = "true" +db-tx-end = "commit-allow-override" +db-uri = "" +jwt-aud = "https://example.org" +jwt-role-claim-key = ".\"a\".\"role\"" +jwt-secret = "REALLYREALLYREALLYREALLYVERYSAFE" +jwt-secret-is-base64 = true +log-level = "info" +openapi-server-proxy-uri = "https://example.org/api" +raw-media-types = "application/vnd.pgrst.db-config" +server-host = "0.0.0.0" +server-port = 80 +server-unix-socket = "/tmp/pgrst_io_test.sock" +server-unix-socket-mode = "777" +app.settings.test = "test" +app.settings.test2 = "test" diff --git a/test/io-tests/configs/expected/no-defaults.config b/test/io-tests/configs/expected/no-defaults.config index 1850156b3..1e3acaaef 100644 --- a/test/io-tests/configs/expected/no-defaults.config +++ b/test/io-tests/configs/expected/no-defaults.config @@ -9,6 +9,7 @@ db-pre-request = "please_run_fast" db-prepared-statements = false db-root-spec = "openapi_v3" db-schemas = "multi,tenant,setup" +db-load-guc-config = "false" db-tx-end = "rollback-allow-override" db-uri = "tmp_db" jwt-aud = "https://postgrest.org" diff --git a/test/io-tests/configs/expected/types.config b/test/io-tests/configs/expected/types.config index 6d6ff1b4c..7f17d6234 100644 --- a/test/io-tests/configs/expected/types.config +++ b/test/io-tests/configs/expected/types.config @@ -9,6 +9,7 @@ db-pre-request = "" db-prepared-statements = true db-root-spec = "" db-schemas = "required" +db-load-guc-config = "true" db-tx-end = "commit" db-uri = "required" jwt-aud = "" diff --git a/test/io-tests/configs/no-defaults-env.yaml b/test/io-tests/configs/no-defaults-env.yaml index e711131ba..e4b4045c0 100644 --- a/test/io-tests/configs/no-defaults-env.yaml +++ b/test/io-tests/configs/no-defaults-env.yaml @@ -11,6 +11,7 @@ PGRST_DB_PREPARED_STATEMENTS: false PGRST_DB_PRE_REQUEST: please_run_fast PGRST_DB_ROOT_SPEC: openapi_v3 PGRST_DB_SCHEMAS: multi, tenant,setup +PGRST_DB_LOAD_GUC_CONFIG: false PGRST_DB_TX_END: rollback-allow-override PGRST_DB_URI: tmp_db PGRST_JWT_AUD: 'https://postgrest.org' diff --git a/test/io-tests/configs/no-defaults.config b/test/io-tests/configs/no-defaults.config index 5e592d5dc..68d8f1d06 100644 --- a/test/io-tests/configs/no-defaults.config +++ b/test/io-tests/configs/no-defaults.config @@ -9,6 +9,7 @@ db-pre-request = "please_run_fast" db-prepared-statements = false db-root-spec = "openapi_v3" db-schemas = "multi, tenant,setup" +db-load-guc-config = "false" db-tx-end = "rollback-allow-override" db-uri = "tmp_db" jwt-aud = "https://postgrest.org" diff --git a/test/io-tests/configs/role-claim-key.config b/test/io-tests/configs/role-claim-key.config index a0bd6605a..658ae9c6a 100644 --- a/test/io-tests/configs/role-claim-key.config +++ b/test/io-tests/configs/role-claim-key.config @@ -1,3 +1,4 @@ db-pool = 1 jwt-role-claim-key = "$(ROLE_CLAIM_KEY)" jwt-secret = "reallyreallyreallyreallyverysafe" +db-load-guc-config = false diff --git a/test/io-tests/configs/secret-from-file.config b/test/io-tests/configs/secret-from-file.config index a80404bda..09d775453 100644 --- a/test/io-tests/configs/secret-from-file.config +++ b/test/io-tests/configs/secret-from-file.config @@ -3,3 +3,4 @@ db-pool = 1 # Read secret from a file: /dev/stdin (alias for standard input) jwt-secret = "@/dev/stdin" jwt-secret-is-base64 = false +db-load-guc-config = false diff --git a/test/io-tests/configs/sigusr2-settings.config b/test/io-tests/configs/sigusr2-settings.config index 1ebfdc148..5c129b0c7 100644 --- a/test/io-tests/configs/sigusr2-settings.config +++ b/test/io-tests/configs/sigusr2-settings.config @@ -3,3 +3,4 @@ db-pool = 1 app.settings.name_var = "John" jwt-secret = "invalidinvalidinvalidinvalidinvalid" +db-load-guc-config = false diff --git a/test/io-tests/configs/simple.config b/test/io-tests/configs/simple.config index 5073cce00..73da75b9f 100644 --- a/test/io-tests/configs/simple.config +++ b/test/io-tests/configs/simple.config @@ -1,2 +1,3 @@ db-pool = 1 jwt-secret = "reallyreallyreallyreallyverysafe" +db-load-guc-config = false diff --git a/test/io-tests/configs/unix-socket.config b/test/io-tests/configs/unix-socket.config index cf45b6eb0..904d0e029 100644 --- a/test/io-tests/configs/unix-socket.config +++ b/test/io-tests/configs/unix-socket.config @@ -1,3 +1,4 @@ db-pool = 1 server-unix-socket = "$(POSTGREST_TEST_SOCKET)" jwt-secret = "reallyreallyreallyreallyverysafe" +db-load-guc-config = false diff --git a/test/io-tests/test_io.py b/test/io-tests/test_io.py index fbfebf34a..2976db5c1 100644 --- a/test/io-tests/test_io.py +++ b/test/io-tests/test_io.py @@ -87,6 +87,7 @@ def defaultenv(): "PGRST_DB_URI": os.environ["PGRST_DB_URI"], "PGRST_DB_SCHEMAS": os.environ["PGRST_DB_SCHEMAS"], "PGRST_DB_ANON_ROLE": os.environ["PGRST_DB_ANON_ROLE"], + "PGRST_DB_LOAD_GUC_CONFIG": "false" } @@ -234,7 +235,13 @@ def test_cli(args, env, use_defaultenv, expect, defaultenv): @pytest.mark.parametrize( - "expectedconfig", (CONFIGSDIR / "expected").iterdir(), ids=attrgetter("name") + "expectedconfig", + [ + expectedconfig + for expectedconfig in (CONFIGSDIR / "expected").iterdir() + if (CONFIGSDIR / expectedconfig.name).exists() + ], + ids=attrgetter("name"), ) def test_expected_config(expectedconfig): """ @@ -261,6 +268,23 @@ def test_expected_config_from_environment(): assert dumpconfig(env=env) == expected +def test_expected_config_from_db_settings(defaultenv): + "Config should be overriden from database settings" + + config = CONFIGSDIR / "no-defaults.config" + env = { + **defaultenv, + "PGRST_DB_LOAD_GUC_CONFIG": "true", + } + expected = ( + (CONFIGSDIR / "expected" / "no-defaults-with-db.config") + .read_text() + .replace("", env["PGRST_DB_URI"]) + ) + + assert dumpconfig(configpath=config, env=env) == expected + + @pytest.mark.parametrize( "config", [conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".config"], @@ -482,3 +506,28 @@ def test_db_schema_reload(tmp_path, defaultenv): response = postgrest.session.get("/parents", headers=headers) assert response.status_code == 200 + + +def test_max_rows_reload(defaultenv): + "max-rows should be reloaded from role settings when PostgREST receives a SIGUSR2." + config = CONFIGSDIR / "sigusr2-settings.config" + + env = { + **defaultenv, + "PGRST_DB_LOAD_GUC_CONFIG": "true", + } + + with run(config, env=env) as postgrest: + response = postgrest.session.head("/projects") + assert response.headers["Content-Range"] == "0-4/*" + + # change max-rows config on the db + postgrest.session.post("/rpc/change_max_rows_config", data={"val": 1}) + + # reload config + postgrest.process.send_signal(signal.SIGUSR2) + + time.sleep(0.1) + + response = postgrest.session.head("/projects") + assert response.headers["Content-Range"] == "0-0/*" diff --git a/test/memory-tests.sh b/test/memory-tests.sh index 790f7c5c7..ebc4a01e8 100755 --- a/test/memory-tests.sh +++ b/test/memory-tests.sh @@ -12,6 +12,7 @@ export PGRST_DB_POOL="1" export PGRST_SERVER_HOST="127.0.0.1" export PGRST_SERVER_PORT="$pgrPort" export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe" +export PGRST_DB_LOAD_GUC_CONFIG="false" trap "kill 0" int term exit