diff --git a/main/Main.hs b/main/Main.hs index 7af31d498..122b158be 100644 --- a/main/Main.hs +++ b/main/Main.hs @@ -27,8 +27,9 @@ import Network.Wai.Handler.Warp (defaultSettings, runSettings, import System.IO (BufferMode (..), hSetBuffering) import PostgREST.App (postgrest) -import PostgREST.Config (AppConfig (..), configPoolTimeout', - prettyVersion, readPathShowHelp, +import PostgREST.Config (AppConfig (..), CLI (..), Command (..), + configPoolTimeout', dumpAppConfig, + prettyVersion, readCLIShowHelp, readValidateConfig) import PostgREST.DbStructure (getDbStructure, getPgVersion) import PostgREST.Error (PgError (PgError), checkIsFatal, @@ -57,10 +58,13 @@ main = do hSetBuffering stderr NoBuffering -- read path from commad line - path <- readPathShowHelp + opts <- readCLIShowHelp -- build the 'AppConfig' from the config file path - conf <- readValidateConfig path + conf <- readValidateConfig $ cliPath opts + + -- dump config and exit if option is set + when (cliCommand opts == CmdDumpConfig) $ dumpAppConfig conf -- These are config values that can't be reloaded at runtime. Reloading some of them would imply restarting the web server. let @@ -122,7 +126,7 @@ main = do -- Re-read the config on SIGUSR2 void $ installHandler sigUSR2 ( - Catch $ reReadConfig path refConf + Catch $ reReadConfig (cliPath opts) refConf ) Nothing #endif diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 667794a3d..bee9d7d06 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -19,9 +19,12 @@ Other hardcoded options such as the minimum version number also belong here. module PostgREST.Config ( prettyVersion , docsVersion + , CLI (..) + , Command (..) , AppConfig (..) , configPoolTimeout' - , readPathShowHelp + , dumpAppConfig + , readCLIShowHelp , readValidateConfig ) where @@ -30,24 +33,25 @@ import qualified Data.ByteString as B import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as BS import qualified Data.Configurator as C -import Data.Either.Combinators (whenLeft) import qualified Text.PrettyPrint.ANSI.Leijen as L -import Control.Lens (preview) -import Control.Monad (fail) -import Crypto.JWT (JWKSet, StringOrURI, stringOrUri) -import Data.List.NonEmpty (fromList) -import Data.Scientific (floatingOrInteger) -import Data.Text (dropEnd, dropWhileEnd, intercalate, pack, - replace, splitOn, strip, stripPrefix, take, - unpack) -import Data.Text.IO (hPutStrLn) -import Data.Version (versionBranch) -import Development.GitRev (gitHash) -import Numeric (readOct) -import Paths_postgrest (version) -import System.IO.Error (IOError) -import System.Posix.Types (FileMode) +import Control.Lens (preview) +import Control.Monad (fail) +import Crypto.JWT (JWKSet, StringOrURI, stringOrUri) +import Data.Aeson (encode, toJSON) +import Data.Either.Combinators (fromRight', whenLeft) +import Data.List.NonEmpty (fromList, toList) +import Data.Scientific (floatingOrInteger) +import Data.Text (dropEnd, dropWhileEnd, intercalate, + pack, replace, splitOn, strip, + stripPrefix, take, toLower, unpack) +import Data.Text.IO (hPutStrLn) +import Data.Version (versionBranch) +import Development.GitRev (gitHash) +import Numeric (readOct, showOct) +import Paths_postgrest (version) +import System.IO.Error (IOError) +import System.Posix.Types (FileMode) import Control.Applicative import Data.Monoid @@ -62,9 +66,15 @@ import PostgREST.Types (JSPath, JSPathExp (..), LogLevel (..)) import Protolude hiding (concat, hPutStrLn, intercalate, null, replace, take, - toS, (<>)) + toList, toLower, toS, (<>)) import Protolude.Conv (toS) +-- | Command line interface options +data CLI = CLI + { cliCommand :: Command + , cliPath :: FilePath } + +data Command = CmdRun | CmdDumpConfig deriving (Eq) -- | Config file settings for the server data AppConfig = AppConfig { @@ -103,6 +113,7 @@ data AppConfig = AppConfig { , configDbPrepared :: Bool } + deriving (Show) configPoolTimeout' :: (Fractional a) => AppConfig -> a configPoolTimeout' = @@ -122,13 +133,13 @@ prettyVersion = docsVersion :: Text docsVersion = "v" <> dropEnd 1 (dropWhileEnd (/= '.') prettyVersion) --- | Read config the file path from the command line. Also prints help. -readPathShowHelp :: IO FilePath -readPathShowHelp = customExecParser parserPrefs opts +-- | Read command line interface options. Also prints help. +readCLIShowHelp :: IO CLI +readCLIShowHelp = customExecParser parserPrefs opts where parserPrefs = prefs showHelpOnError - opts = info (helper <*> pathParser) $ + opts = info (helper <*> cliParser) $ fullDesc <> progDesc ( "PostgREST " @@ -140,11 +151,16 @@ readPathShowHelp = customExecParser parserPrefs opts L.<> nest 2 (hardline L.<> exampleCfg) ) - pathParser :: Parser FilePath - pathParser = - strArgument $ + cliParser :: Parser CLI + cliParser = CLI <$> + flag CmdRun CmdDumpConfig ( + long "dump-config" <> + help "Dump loaded configuration and exit" + ) <*> + strArgument ( metavar "FILENAME" <> help "Path to configuration file" + ) exampleCfg :: Doc exampleCfg = vsep . map (text . toS) . lines $ @@ -225,6 +241,62 @@ readPathShowHelp = customExecParser parserPrefs opts |log-level = "error" |] +-- | Dump the config +dumpAppConfig :: AppConfig -> IO () +dumpAppConfig conf = do + putStr dump + exitSuccess + + where + dump = unlines $ (\(k, v) -> k <> " = " <> v) <$> + pgrstSettings ++ appSettings + + -- apply conf to all pgrst settings + pgrstSettings = (\(k, v) -> (k, v conf)) <$> + [("db-uri", q . configDbUri) + ,("db-schema", q . intercalate "," . toList . configSchemas) + ,("db-anon-role", q . configAnonRole) + ,("db-pool", show . configPoolSize) + ,("db-pool-timeout", show . configPoolTimeout) + ,("db-extra-search-path", q . intercalate "," . configExtraSearchPath) + ,("db-channel", q . configDbChannel) + ,("db-channel-enabled", toLower . show . configDbChannelEnabled) + ,("db-tx-end", q . showTxEnd) + ,("db-prepared-statements", toLower . show . configDbPrepared) + ,("server-host", q . configHost) + ,("server-port", show . configPort) + ,("server-unix-socket", q . maybe mempty pack . configSocket) + ,("server-unix-socket-mode", q . pack . showSocketMode) + ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenAPIProxyUri) + ,("jwt-secret", q . toS . showJwtSecret) + ,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience) + ,("secret-is-base64", toLower . show . configJwtSecretIsBase64) + ,("role-claim-key", q . intercalate mempty . fmap show . fromRight' . configRoleClaimKey) + ,("max-rows", maybe "\"\"" show . configMaxRows) + ,("pre-request", q . fromMaybe mempty . configPreReq) + ,("root-spec", q . fromMaybe mempty . configRootSpec) + ,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes) + ,("log-level", q . show . configLogLevel) + ] + + -- quote all app.settings + appSettings = second q <$> configSettings conf + + -- quote strings and replace " with \" + q s = "\"" <> replace "\"" "\\\"" s <> "\"" + + showTxEnd c = case (configTxRollbackAll c, configTxAllowOverride c) of + ( False, False ) -> "commit" + ( False, True ) -> "commit-allow-override" + ( True , False ) -> "rollback" + ( True , True ) -> "rollback-allow-override" + showSocketMode c = showOct (fromRight' $ configSocketMode c) "" + showJwtSecret c + | configJwtSecretIsBase64 c = B64.encode secret + | otherwise = toS secret + where + secret = fromMaybe mempty $ configJwtSecret c + -- | Parse the config file readAppConfig :: FilePath -> IO AppConfig readAppConfig cfgPath = do diff --git a/src/PostgREST/Types.hs b/src/PostgREST/Types.hs index 61dbeda7a..a2637ebed 100644 --- a/src/PostgREST/Types.hs +++ b/src/PostgREST/Types.hs @@ -546,7 +546,12 @@ sourceCTEName = "pgrst_source" -- | full jspath, e.g. .property[0].attr.detail type JSPath = [JSPathExp] -- | jspath expression, e.g. .property, .property[0] or ."property-dash" -data JSPathExp = JSPKey Text | JSPIdx Int deriving (Eq, Show) +data JSPathExp = JSPKey Text | JSPIdx Int deriving (Eq) + +instance Show JSPathExp where + -- TODO: this needs to be quoted properly for special chars + show (JSPKey k) = "." <> show k + show (JSPIdx i) = "[" <> show i <> "]" -- | Current database connection status data ConnectionStatus data ConnectionStatus @@ -555,4 +560,10 @@ data ConnectionStatus | FatalConnectionError Text deriving (Eq, Show) -data LogLevel = LogCrit | LogError | LogWarn | LogInfo deriving (Eq, Show) +data LogLevel = LogCrit | LogError | LogWarn | LogInfo deriving (Eq) + +instance Show LogLevel where + show LogCrit = "crit" + show LogError = "error" + show LogWarn = "warn" + show LogInfo = "info" diff --git a/test/io-tests.sh b/test/io-tests.sh index 703ea0a33..1b15334f7 100755 --- a/test/io-tests.sh +++ b/test/io-tests.sh @@ -47,6 +47,54 @@ ok(){ result 'ok' "- $1"; } ko(){ result 'not ok' "- $1"; failedTests=$(( $failedTests + 1 )); } comment(){ echo "# $1"; } +######################## +# SYNCHRONOUS IO TESTS # +######################## + +dumpedConfigMatchesExpectation(){ + # This test compares the dumped config vs. the corresponding file in ./configs/expected. + # To be used to test default values, config aliases and environment variables. + dump="$(mktemp)" + tap(){ + if test $1 -eq 0; then + ok "dump of config file $2 does match expectation" + else + ko "dump of config file $2 does not match expectation" + fi + rm -f "$dump" + } + trap 'tap $? $1; trap - RETURN; return 0' ERR RETURN + postgrest --dump-config "$1" > "$dump" + diff "$dump" "$2" +} + +dumpedConfigIsValid(){ + # This test compares the dumped config vs. the dumped-reread-redumped config. + # Re-reading the dumped config tests the validity of the config format. + # Re-dumping this config should yield no difference to the first dump, showing + # that the semantics have not changed by dumping. + # Note: only dump vs redump must be equal, the original config file can be different, + # because of default values, whitespace, and quoting + dump="$(mktemp)" + redump="$(mktemp)" + tap(){ + if test $1 -eq 0; then + ok "dump of config file $2 is valid" + else + ko "dump of config file $2 is invalid" + fi + rm -f "$dump" "$redump" + } + trap 'tap $? $1; trap - RETURN; return 0' ERR RETURN + postgrest --dump-config "$1" > "$dump" + postgrest --dump-config "$dump" > "$redump" + diff "$dump" "$redump" +} + +#################### +# BACKGROUND TESTS # +#################### + # Utilities to start/stop test PostgREST server running in the background pgrStart(){ # stderr is not piped to /dev/null to catch errors on startup. @@ -309,6 +357,23 @@ psql -l "$POSTGREST_TEST_CONNECTION" 1>/dev/null 2>/dev/null || bailOut 'postgre echo "Running IO tests.." +# run dumpConfigIsValid with as many inputs as possible +for cfg in configs/*.config +do + # ROLE_CLAIM_KEY is only used in one of the config files + # using a complex example here, to make sure the quoting works + ROLE_CLAIM_KEY='."https://www.example.com/roles"[0].value' \ + dumpedConfigIsValid "$cfg" \ + <<< "Y29ubmVjdGlvbl9zdHJpbmc=" # /dev/stdin is read by some config files, one of them expects Base64 +done + +# run dumpConfigMatchesExpectation with all expectations +for exp in configs/expected/*.config +do + cfg="$(sed -e 's|expected/||' <(echo $exp))" + dumpedConfigMatchesExpectation "$cfg" "$exp" +done + socketConnection readSecretFromFile word.noeol 'simple (no EOL)' diff --git a/test/io-tests/configs/defaults.config b/test/io-tests/configs/defaults.config new file mode 100644 index 000000000..c02668beb --- /dev/null +++ b/test/io-tests/configs/defaults.config @@ -0,0 +1,3 @@ +db-uri = "required" +db-schema = "required" +db-anon-role = "required" diff --git a/test/io-tests/configs/expected/defaults.config b/test/io-tests/configs/expected/defaults.config new file mode 100644 index 000000000..6fac24806 --- /dev/null +++ b/test/io-tests/configs/expected/defaults.config @@ -0,0 +1,24 @@ +db-uri = "required" +db-schema = "required" +db-anon-role = "required" +db-pool = 10 +db-pool-timeout = 10 +db-extra-search-path = "public" +db-channel = "pgrst" +db-channel-enabled = false +db-tx-end = "commit" +db-prepared-statements = true +server-host = "!4" +server-port = 3000 +server-unix-socket = "" +server-unix-socket-mode = "660" +openapi-server-proxy-uri = "" +jwt-secret = "" +jwt-aud = "" +secret-is-base64 = false +role-claim-key = ".\"role\"" +max-rows = "" +pre-request = "" +root-spec = "" +raw-media-types = "" +log-level = "error" diff --git a/test/io-tests/configs/expected/no-defaults.config b/test/io-tests/configs/expected/no-defaults.config new file mode 100644 index 000000000..b8b64f306 --- /dev/null +++ b/test/io-tests/configs/expected/no-defaults.config @@ -0,0 +1,26 @@ +db-uri = "tmp_db" +db-schema = "multi,tenant,setup" +db-anon-role = "root" +db-pool = 1 +db-pool-timeout = 100 +db-extra-search-path = "public,test" +db-channel = "postgrest" +db-channel-enabled = true +db-tx-end = "rollback-allow-override" +db-prepared-statements = false +server-host = "0.0.0.0" +server-port = 80 +server-unix-socket = "/tmp/pgrst_io_test.sock" +server-unix-socket-mode = "777" +openapi-server-proxy-uri = "https://postgrest.org" +jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" +jwt-aud = "https://postgrest.org" +secret-is-base64 = true +role-claim-key = ".\"user\"[0].\"real-role\"" +max-rows = 1000 +pre-request = "please_run_fast" +root-spec = "openapi_v3" +raw-media-types = "application/vnd.pgrst.config" +log-level = "info" +app.settings.test = "test" +app.settings.test2 = "test" diff --git a/test/io-tests/configs/no-defaults.config b/test/io-tests/configs/no-defaults.config new file mode 100644 index 000000000..d02bba036 --- /dev/null +++ b/test/io-tests/configs/no-defaults.config @@ -0,0 +1,26 @@ +db-uri = "tmp_db" +db-schema = "multi, tenant,setup" +db-anon-role = "root" +db-pool = 1 +db-pool-timeout = 100 +db-extra-search-path = "public, test" +db-channel = "postgrest" +db-channel-enabled = true +db-tx-end = "rollback-allow-override" +db-prepared-statements = false +server-host = "0.0.0.0" +server-port = 80 +server-unix-socket = "/tmp/pgrst_io_test.sock" +server-unix-socket-mode = "777" +openapi-server-proxy-uri = "https://postgrest.org" +jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5" +jwt-aud = "https://postgrest.org" +secret-is-base64 = true +role-claim-key = ".user[0].\"real-role\"" +max-rows = 1000 +pre-request = "please_run_fast" +root-spec = "openapi_v3" +raw-media-types = "application/vnd.pgrst.config" +log-level = "info" +app.settings.test = "test" +app.settings.test2 = "test"