feat: get configuration parameters from the db
Allows configuring postgrest from the db by setting config parameters on the connection role. For example: ALTER ROLE postgrest_test_authenticator SET pgrst.jwt-secret = "REALLYREALLYREALLYREALLYVERYSAFE" The above wWill set the `jwt-secret` config option accordingly. SUPERUSER privileges are required for ALTERing role settings, so this might not work on some cloud-managed databases. This feature is enabled by default, for disabling it you can add the following to the config file: db-load-guc-config = false
This commit is contained in:
committed by
Steve Chavez
parent
674615041a
commit
9c005fc683
+59
-53
@@ -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)
|
||||
|
||||
+35
-22
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+29
@@ -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';
|
||||
|
||||
Vendored
+7
@@ -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 ;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,3 +2,4 @@ db-pool = 1
|
||||
db-pool-timeout = 1
|
||||
|
||||
app.settings.external_api_secret = "0123456789abcdef"
|
||||
db-load-guc-config = false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
db-uri = "@/dev/stdin"
|
||||
db-pool = 1
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
db-load-guc-config = false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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 = "<REPLACED_WITH_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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
db-pool = 1
|
||||
jwt-role-claim-key = "$(ROLE_CLAIM_KEY)"
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
db-load-guc-config = false
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,3 +3,4 @@ db-pool = 1
|
||||
|
||||
app.settings.name_var = "John"
|
||||
jwt-secret = "invalidinvalidinvalidinvalidinvalid"
|
||||
db-load-guc-config = false
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
db-pool = 1
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
db-load-guc-config = false
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
db-pool = 1
|
||||
server-unix-socket = "$(POSTGREST_TEST_SOCKET)"
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
db-load-guc-config = false
|
||||
|
||||
@@ -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("<REPLACED_WITH_DB_URI>", 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/*"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user