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:
steve-chavez
2021-01-19 13:49:40 -05:00
committed by Steve Chavez
parent 674615041a
commit 9c005fc683
29 changed files with 250 additions and 76 deletions
+59 -53
View File
@@ -8,7 +8,7 @@ import qualified Data.ByteString.Lazy as LBS
import qualified Hasql.Connection as C import qualified Hasql.Connection as C
import qualified Hasql.Notifications as N import qualified Hasql.Notifications as N
import qualified Hasql.Pool as P 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 qualified Hasql.Transaction.Sessions as HT
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
@@ -28,8 +28,7 @@ import Data.Time.Clock (getCurrentTime)
import Network.Wai.Handler.Warp (defaultSettings, runSettings, import Network.Wai.Handler.Warp (defaultSettings, runSettings,
setHost, setPort, setServerName) setHost, setPort, setServerName)
import System.CPUTime (getCPUTime) import System.CPUTime (getCPUTime)
import System.IO (BufferMode (..), hPrint, import System.IO (BufferMode (..), hSetBuffering)
hSetBuffering)
import Text.Printf (hPrintf) import Text.Printf (hPrintf)
import PostgREST.App (postgrest) import PostgREST.App (postgrest)
@@ -37,6 +36,7 @@ import PostgREST.Config
import PostgREST.DbStructure (getDbStructure, getPgVersion) import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Error (PgError (PgError), checkIsFatal, import PostgREST.Error (PgError (PgError), checkIsFatal,
errorPayload) errorPayload)
import PostgREST.Statements (dbSettingsStatement)
import PostgREST.Types (ConnectionStatus (..), DbStructure, import PostgREST.Types (ConnectionStatus (..), DbStructure,
PgVersion (..), SCacheStatus (..), PgVersion (..), SCacheStatus (..),
minimumPgVersion) minimumPgVersion)
@@ -68,7 +68,7 @@ main = do
opts <- readCLIShowHelp env opts <- readCLIShowHelp env
-- build the 'AppConfig' from the config file path -- 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. -- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server.
let let
@@ -88,19 +88,7 @@ main = do
poolSize = configDbPoolSize conf poolSize = configDbPoolSize conf
poolTimeout = configDbPoolTimeout' conf poolTimeout = configDbPoolTimeout' conf
logLevel = configLogLevel conf logLevel = configLogLevel conf
gucConfigEnabled = configDbLoadGucConfig conf
case cliCommand opts of
CmdDumpConfig ->
do
putStr $ dumpAppConfig conf
exitSuccess
CmdDumpSchema ->
do
dumpedSchema <- dumpSchema conf
putStrLn dumpedSchema
exitSuccess
CmdRun ->
pass
-- create connection pool with the provided settings, returns either a 'Connection' or a 'ConnectionError'. Does not throw. -- create connection pool with the provided settings, returns either a 'Connection' or a 'ConnectionError'. Does not throw.
pool <- P.acquire (poolSize, poolTimeout, dbUri) pool <- P.acquire (poolSize, poolTimeout, dbUri)
@@ -117,6 +105,24 @@ main = do
-- Config that can change at runtime -- Config that can change at runtime
refConf <- newIORef conf 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. -- This is passed to the connectionWorker method so it can kill the main thread if the PostgreSQL's version is not supported.
mainTid <- myThreadId mainTid <- myThreadId
@@ -141,11 +147,10 @@ main = do
Catch connWorker Catch connWorker
) Nothing ) Nothing
-- Re-read the config on SIGUSR2, but only if we have a config file -- Re-read the config on SIGUSR2
when (isJust $ cliPath opts) $ void $ installHandler sigUSR2 (
void $ installHandler sigUSR2 ( Catch $ reReadConfig pool gucConfigEnabled env (cliPath opts) refConf >> putStrLn ("Config reloaded" :: Text)
Catch $ reReadConfig env (cliPath opts) refConf ) Nothing
) Nothing
#endif #endif
-- reload schema cache on NOTIFY -- reload schema cache on NOTIFY
@@ -267,6 +272,15 @@ connectionStatus pool =
putStrLn $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..." putStrLn $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
return itShould 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. -- | Load the DbStructure by using a connection from the pool.
loadSchemaCache :: P.Pool -> PgVersion -> IORef AppConfig -> IORef (Maybe DbStructure) -> IO SCacheStatus loadSchemaCache :: P.Pool -> PgVersion -> IORef AppConfig -> IORef (Maybe DbStructure) -> IO SCacheStatus
loadSchemaCache pool actualPgVersion refConf refDbStructure = do 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 errorMessage = "Could not listen for notifications on the " <> dbChannel <> " channel" :: Text
retryMessage = "Retrying listening 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.
-- | Re-reads the config at runtime. Invoked on SIGUSR2.
-- | If it panics(config path was changed, invalid setting), it'll show an error but won't kill the main thread. -- | 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 :: P.Pool -> Bool -> Environment -> Maybe FilePath -> IORef AppConfig -> IO ()
reReadConfig env path refConf = do reReadConfig pool gucConfigEnabled env path refConf = do
conf <- readValidateConfig env path dbSettings <- if gucConfigEnabled then loadDbSettings pool else pure []
conf <- readValidateConfig dbSettings env path
atomicWriteIORef refConf conf atomicWriteIORef refConf conf
putStrLn ("Config file reloaded" :: Text)
#endif
-- | Dump DbStructure schema to JSON -- | Dump DbStructure schema to JSON
dumpSchema :: AppConfig -> IO LBS.ByteString dumpSchema :: P.Pool -> AppConfig -> IO LBS.ByteString
dumpSchema conf = dumpSchema pool conf = do
do result <-
eitherConn <- C.acquire . toS $ configDbUri conf timeToStderr "Loaded schema in %.3f seconds" $
case eitherConn of P.use pool $ do
Left e -> hPrint stderr e >> exitFailure pgVersion <- getPgVersion
Right conn -> do HT.transaction HT.ReadCommitted HT.Read $
result <- getDbStructure
timeToStderr "Loaded schema in %.3f seconds" $ (toList $ configDbSchemas conf)
flip S.run conn $ do (configDbExtraSearchPath conf)
pgVersion <- getPgVersion pgVersion
HT.transaction HT.ReadCommitted HT.Read $ (configDbPreparedStatements conf)
getDbStructure P.release pool
(toList $ configDbSchemas conf) case result of
(configDbExtraSearchPath conf) Left e -> do
pgVersion hPutStrLn stderr $ "An error ocurred when loading the schema cache:\n" <> show e
(configDbPreparedStatements conf) exitFailure
C.release conn Right dbStructure -> return $ Aeson.encode dbStructure
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 -- | 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) timeToStderr :: [Char] -> IO (Either a b) -> IO (Either a b)
+35 -22
View File
@@ -45,6 +45,7 @@ import Control.Monad (fail)
import Crypto.JWT (JWKSet, StringOrURI, stringOrUri) import Crypto.JWT (JWKSet, StringOrURI, stringOrUri)
import Data.Aeson (encode, toJSON) import Data.Aeson (encode, toJSON)
import Data.Either.Combinators (fromRight', whenLeft) import Data.Either.Combinators (fromRight', whenLeft)
import Data.List (lookup)
import Data.List.NonEmpty (fromList, toList) import Data.List.NonEmpty (fromList, toList)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.Scientific (floatingOrInteger) import Data.Scientific (floatingOrInteger)
@@ -101,6 +102,7 @@ data AppConfig = AppConfig {
, configDbPreparedStatements :: Bool , configDbPreparedStatements :: Bool
, configDbRootSpec :: Maybe Text , configDbRootSpec :: Maybe Text
, configDbSchemas :: NonEmpty Text , configDbSchemas :: NonEmpty Text
, configDbLoadGucConfig :: Bool
, configDbTxAllowOverride :: Bool , configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool , configDbTxRollbackAll :: Bool
, configDbUri :: Text , configDbUri :: Text
@@ -215,6 +217,9 @@ readCLIShowHelp env = customExecParser parserPrefs opts
|## Enable or disable the notification channel |## Enable or disable the notification channel
|db-channel-enabled = false |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 |## how to terminate database transactions
|## possible values are: |## possible values are:
|## commit (default) |## commit (default)
@@ -280,6 +285,7 @@ dumpAppConfig conf =
,("db-prepared-statements", toLower . show . configDbPreparedStatements) ,("db-prepared-statements", toLower . show . configDbPreparedStatements)
,("db-root-spec", q . fromMaybe mempty . configDbRootSpec) ,("db-root-spec", q . fromMaybe mempty . configDbRootSpec)
,("db-schemas", q . intercalate "," . toList . configDbSchemas) ,("db-schemas", q . intercalate "," . toList . configDbSchemas)
,("db-load-guc-config", q . toLower . show . configDbLoadGucConfig)
,("db-tx-end", q . showTxEnd) ,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri) ,("db-uri", q . configDbUri)
,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience) ,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience)
@@ -313,7 +319,7 @@ dumpAppConfig conf =
secret = fromMaybe mempty $ configJwtSecret c secret = fromMaybe mempty $ configJwtSecret c
showSocketMode c = showOct (fromRight' $ configServerUnixSocketMode 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 -- because C.required and C.optional have different signatures
class JustIfMaybe a b where class JustIfMaybe a b where
justIfMaybe :: a -> b justIfMaybe :: a -> b
@@ -325,8 +331,8 @@ instance JustIfMaybe a (Maybe a) where
justIfMaybe a = Just a justIfMaybe a = Just a
-- | Parse the config file -- | Parse the config file
readAppConfig :: Environment -> Maybe FilePath -> IO AppConfig readAppConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig
readAppConfig env optPath = do readAppConfig dbSettings env optPath = do
-- Now read the actual config file -- Now read the actual config file
conf <- case optPath of conf <- case optPath of
Just cfgPath -> catches (C.load cfgPath) Just cfgPath -> catches (C.load cfgPath)
@@ -356,12 +362,13 @@ readAppConfig env optPath = do
<*> (fromMaybe 10 <$> optInt "db-pool-timeout") <*> (fromMaybe 10 <$> optInt "db-pool-timeout")
<*> optWithAlias (optString "db-pre-request") <*> optWithAlias (optString "db-pre-request")
(optString "pre-request") (optString "pre-request")
<*> (fromMaybe True <$> optBool "db-prepared-statements") <*> (fromMaybe True <$> optBool "db-prepared-statements")
<*> optWithAlias (optString "db-root-spec") <*> optWithAlias (optString "db-root-spec")
(optString "root-spec") (optString "root-spec")
<*> (fromList . splitOnCommas <$> reqWithAlias (optValue "db-schemas") <*> (fromList . splitOnCommas <$> reqWithAlias (optValue "db-schemas")
(optValue "db-schema") (optValue "db-schema")
"missing key: either db-schemas or db-schema must be set") "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" snd
<*> parseTxEnd "db-tx-end" fst <*> parseTxEnd "db-tx-end" fst
<*> reqString "db-uri" <*> reqString "db-uri"
@@ -387,21 +394,27 @@ readAppConfig env optPath = do
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
normalize k = ("app.settings." <>) <$> stripPrefix "PGRST_APP_SETTINGS_" (toS k) 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.Parser C.Value a -> C.Parser C.Config b) ->
C.Key -> (C.Value -> a) -> C.Parser C.Config b C.Key -> (C.Value -> a) -> C.Parser C.Config b
overrideFromEnvironment necessity key coercion = overrideFromDbOrEnvironment necessity key coercion =
case M.lookup name env of case reloadableDbSetting <|> M.lookup name env of
Just envVal -> pure $ justIfMaybe $ coercion $ C.String envVal Just dbOrEnvVal -> pure $ justIfMaybe $ coercion $ C.String dbOrEnvVal
Nothing -> necessity key (coercion <$> C.value) Nothing -> necessity key (coercion <$> C.value)
where where
name = "PGRST_" <> map capitalize (toS key) name = "PGRST_" <> map capitalize (toS key)
capitalize '-' = '_' capitalize '-' = '_'
capitalize c = toUpper c 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 :: C.Key -> C.Parser C.Config (Either Text FileMode)
parseSocketFileMode k = 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 Nothing -> pure $ Right 432 -- return default 660 mode if no value was provided
Just fileModeText -> Just fileModeText ->
case (readOct . unpack) fileModeText of case (readOct . unpack) fileModeText of
@@ -414,7 +427,7 @@ readAppConfig env optPath = do
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI) parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
parseJwtAudience k = parseJwtAudience k =
overrideFromEnvironment C.optional k coerceText >>= \case overrideFromDbOrEnvironment C.optional k coerceText >>= \case
Nothing -> pure Nothing -- no audience in config file Nothing -> pure Nothing -- no audience in config file
Just aud -> case preview stringOrUri (unpack aud) of Just aud -> case preview stringOrUri (unpack aud) of
Nothing -> fail "Invalid Jwt audience. Check your configuration." 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 :: C.Key -> C.Parser C.Config LogLevel
parseLogLevel k = parseLogLevel k =
overrideFromEnvironment C.optional k coerceText >>= \case overrideFromDbOrEnvironment C.optional k coerceText >>= \case
Nothing -> pure LogError Nothing -> pure LogError
Just "" -> pure LogError Just "" -> pure LogError
Just "crit" -> pure LogCrit Just "crit" -> pure LogCrit
@@ -434,7 +447,7 @@ readAppConfig env optPath = do
parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool
parseTxEnd k f = parseTxEnd k f =
overrideFromEnvironment C.optional k coerceText >>= \case overrideFromDbOrEnvironment C.optional k coerceText >>= \case
-- RollbackAll AllowOverride -- RollbackAll AllowOverride
Nothing -> pure $ f (False, False) Nothing -> pure $ f (False, False)
Just "" -> pure $ f (False, False) Just "" -> pure $ f (False, False)
@@ -460,19 +473,19 @@ readAppConfig env optPath = do
Nothing -> alias Nothing -> alias
reqString :: C.Key -> C.Parser C.Config Text 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 :: 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 :: 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 :: (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 :: 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.Value -> Text
coerceText (C.String s) = s coerceText (C.String s) = s
@@ -506,10 +519,10 @@ readAppConfig env optPath = do
hPutStrLn stderr err hPutStrLn stderr err
exitFailure exitFailure
-- | Parse the AppConfig and validate it. Panic on invalid config options. -- | Parse the AppConfig and validate it. Overrides the config options from env vars or db settings. Panics on invalid config options.
readValidateConfig :: Environment -> Maybe FilePath -> IO AppConfig readValidateConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig
readValidateConfig env path = do readValidateConfig dbSettings env path = do
conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig env path conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig dbSettings env path
-- Checks that the provided proxy uri is formated correctly -- Checks that the provided proxy uri is formated correctly
when (isMalformedProxyUri $ toS <$> configOpenApiServerProxyUri conf) $ when (isMalformedProxyUri $ toS <$> configOpenApiServerProxyUri conf) $
panic panic
+20
View File
@@ -14,6 +14,7 @@ module PostgREST.Statements (
, createReadStatement , createReadStatement
, callProcStatement , callProcStatement
, createExplainStatement , createExplainStatement
, dbSettingsStatement
) where ) where
@@ -24,6 +25,7 @@ import qualified Data.ByteString.Char8 as BS
import Data.Maybe import Data.Maybe
import Data.Text.Read (decimal) import Data.Text.Read (decimal)
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H import qualified Hasql.Statement as H
import Network.HTTP.Types.Status import Network.HTTP.Types.Status
import PostgREST.Error import PostgREST.Error
@@ -37,6 +39,8 @@ import Protolude.Conv (toS)
import qualified Hasql.DynamicStatements.Snippet as H import qualified Hasql.DynamicStatements.Snippet as H
import qualified Hasql.DynamicStatements.Statement 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 {-| The generic query result format used by API responses. The location header
is represented as a list of strings containing variable bindings like is represented as a list of strings containing variable bindings like
@"k1=eq.42"@, or the empty list if there is no location header. @"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 :: HD.Value (Either SimpleError (Maybe Status))
decodeGucStatus = first (const GucStatusError) . fmap (Just . toEnum . fst) . decimal <$> HD.text 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
+1
View File
@@ -78,6 +78,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbPreparedStatements = True , configDbPreparedStatements = True
, configDbRootSpec = Nothing , configDbRootSpec = Nothing
, configDbSchemas = fromList ["test"] , configDbSchemas = fromList ["test"]
, configDbLoadGucConfig = False
, configDbUri = mempty , configDbUri = mempty
, configJWKS = parseSecret <$> secret , configJWKS = parseSecret <$> secret
, configJwtAudience = Nothing , configJwtAudience = Nothing
+29
View File
@@ -5,3 +5,32 @@ CREATE ROLE postgrest_test_default_role;
CREATE ROLE postgrest_test_author; CREATE ROLE postgrest_test_author;
GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO :USER; 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';
+7
View File
@@ -1924,3 +1924,10 @@ $$ language sql;
-- Only used for manually testing creating prepared statements -- Only used for manually testing creating prepared statements
create view prepared_statements as create view prepared_statements as
select * from pg_catalog.pg_prepared_statements; 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 ;
+1
View File
@@ -7,3 +7,4 @@ pre-request = "check_alias"
role-claim-key = ".aliased" role-claim-key = ".aliased"
root-spec = "open_alias" root-spec = "open_alias"
secret-is-base64 = true secret-is-base64 = true
db-load-guc-config = false
@@ -2,3 +2,4 @@ db-pool = 1
db-pool-timeout = 1 db-pool-timeout = 1
app.settings.external_api_secret = "0123456789abcdef" 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) # Read secret from a file: /dev/stdin (alias for standard input)
jwt-secret = "@/dev/stdin" jwt-secret = "@/dev/stdin"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
db-load-guc-config = false
@@ -5,3 +5,4 @@ db-anon-role = "required"
db-channel-enabled = "1" db-channel-enabled = "1"
db-prepared-statements = "0" db-prepared-statements = "0"
jwt-secret-is-base64 = "2" jwt-secret-is-base64 = "2"
db-load-guc-config = false
@@ -5,3 +5,4 @@ db-anon-role = "required"
db-channel-enabled = "true" db-channel-enabled = "true"
db-prepared-statements = "FALSE" db-prepared-statements = "FALSE"
jwt-secret-is-base64 = "\"true\"" jwt-secret-is-base64 = "\"true\""
db-load-guc-config = false
@@ -1,3 +1,4 @@
db-uri = "@/dev/stdin" db-uri = "@/dev/stdin"
db-pool = 1 db-pool = 1
jwt-secret = "reallyreallyreallyreallyverysafe" jwt-secret = "reallyreallyreallyreallyverysafe"
db-load-guc-config = false
+2
View File
@@ -1,3 +1,5 @@
db-uri = "required" db-uri = "required"
db-schemas = "required" db-schemas = "required"
db-anon-role = "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-prepared-statements = true
db-root-spec = "open_alias" db-root-spec = "open_alias"
db-schemas = "provided_through_alias" db-schemas = "provided_through_alias"
db-load-guc-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
jwt-aud = "" jwt-aud = ""
@@ -9,6 +9,7 @@ db-pre-request = ""
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "" db-root-spec = ""
db-schemas = "required" db-schemas = "required"
db-load-guc-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
jwt-aud = "" jwt-aud = ""
@@ -9,6 +9,7 @@ db-pre-request = ""
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "" db-root-spec = ""
db-schemas = "required" db-schemas = "required"
db-load-guc-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
jwt-aud = "" jwt-aud = ""
@@ -9,6 +9,7 @@ db-pre-request = ""
db-prepared-statements = true db-prepared-statements = true
db-root-spec = "" db-root-spec = ""
db-schemas = "required" db-schemas = "required"
db-load-guc-config = "false"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
jwt-aud = "" 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-prepared-statements = false
db-root-spec = "openapi_v3" db-root-spec = "openapi_v3"
db-schemas = "multi,tenant,setup" db-schemas = "multi,tenant,setup"
db-load-guc-config = "false"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "tmp_db" db-uri = "tmp_db"
jwt-aud = "https://postgrest.org" jwt-aud = "https://postgrest.org"
@@ -9,6 +9,7 @@ db-pre-request = ""
db-prepared-statements = true db-prepared-statements = true
db-root-spec = "" db-root-spec = ""
db-schemas = "required" db-schemas = "required"
db-load-guc-config = "true"
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "required" db-uri = "required"
jwt-aud = "" jwt-aud = ""
@@ -11,6 +11,7 @@ PGRST_DB_PREPARED_STATEMENTS: false
PGRST_DB_PRE_REQUEST: please_run_fast PGRST_DB_PRE_REQUEST: please_run_fast
PGRST_DB_ROOT_SPEC: openapi_v3 PGRST_DB_ROOT_SPEC: openapi_v3
PGRST_DB_SCHEMAS: multi, tenant,setup PGRST_DB_SCHEMAS: multi, tenant,setup
PGRST_DB_LOAD_GUC_CONFIG: false
PGRST_DB_TX_END: rollback-allow-override PGRST_DB_TX_END: rollback-allow-override
PGRST_DB_URI: tmp_db PGRST_DB_URI: tmp_db
PGRST_JWT_AUD: 'https://postgrest.org' PGRST_JWT_AUD: 'https://postgrest.org'
+1
View File
@@ -9,6 +9,7 @@ db-pre-request = "please_run_fast"
db-prepared-statements = false db-prepared-statements = false
db-root-spec = "openapi_v3" db-root-spec = "openapi_v3"
db-schemas = "multi, tenant,setup" db-schemas = "multi, tenant,setup"
db-load-guc-config = "false"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "tmp_db" db-uri = "tmp_db"
jwt-aud = "https://postgrest.org" jwt-aud = "https://postgrest.org"
@@ -1,3 +1,4 @@
db-pool = 1 db-pool = 1
jwt-role-claim-key = "$(ROLE_CLAIM_KEY)" jwt-role-claim-key = "$(ROLE_CLAIM_KEY)"
jwt-secret = "reallyreallyreallyreallyverysafe" 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) # Read secret from a file: /dev/stdin (alias for standard input)
jwt-secret = "@/dev/stdin" jwt-secret = "@/dev/stdin"
jwt-secret-is-base64 = false jwt-secret-is-base64 = false
db-load-guc-config = false
@@ -3,3 +3,4 @@ db-pool = 1
app.settings.name_var = "John" app.settings.name_var = "John"
jwt-secret = "invalidinvalidinvalidinvalidinvalid" jwt-secret = "invalidinvalidinvalidinvalidinvalid"
db-load-guc-config = false
+1
View File
@@ -1,2 +1,3 @@
db-pool = 1 db-pool = 1
jwt-secret = "reallyreallyreallyreallyverysafe" jwt-secret = "reallyreallyreallyreallyverysafe"
db-load-guc-config = false
+1
View File
@@ -1,3 +1,4 @@
db-pool = 1 db-pool = 1
server-unix-socket = "$(POSTGREST_TEST_SOCKET)" server-unix-socket = "$(POSTGREST_TEST_SOCKET)"
jwt-secret = "reallyreallyreallyreallyverysafe" jwt-secret = "reallyreallyreallyreallyverysafe"
db-load-guc-config = false
+50 -1
View File
@@ -87,6 +87,7 @@ def defaultenv():
"PGRST_DB_URI": os.environ["PGRST_DB_URI"], "PGRST_DB_URI": os.environ["PGRST_DB_URI"],
"PGRST_DB_SCHEMAS": os.environ["PGRST_DB_SCHEMAS"], "PGRST_DB_SCHEMAS": os.environ["PGRST_DB_SCHEMAS"],
"PGRST_DB_ANON_ROLE": os.environ["PGRST_DB_ANON_ROLE"], "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( @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): def test_expected_config(expectedconfig):
""" """
@@ -261,6 +268,23 @@ def test_expected_config_from_environment():
assert dumpconfig(env=env) == expected 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( @pytest.mark.parametrize(
"config", "config",
[conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".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) response = postgrest.session.get("/parents", headers=headers)
assert response.status_code == 200 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/*"
+1
View File
@@ -12,6 +12,7 @@ export PGRST_DB_POOL="1"
export PGRST_SERVER_HOST="127.0.0.1" export PGRST_SERVER_HOST="127.0.0.1"
export PGRST_SERVER_PORT="$pgrPort" export PGRST_SERVER_PORT="$pgrPort"
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe" export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
export PGRST_DB_LOAD_GUC_CONFIG="false"
trap "kill 0" int term exit trap "kill 0" int term exit