diff --git a/CHANGELOG.md b/CHANGELOG.md index 392e6bc50..e8bfa6bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). + New option `db-pool-max-idletime` (default 30s). + This is equivalent to the old option `db-pool-timeout` of PostgREST 10.0.0. + A config alias for `db-pool-timeout` is included. + - #2703, Add pre-config function - @steve-chavez + + New config option `db-pre-config`(empty by default) + + Allows using the in-database configuration without SUPERUSER ## [11.0.1] - 2023-04-27 diff --git a/src/PostgREST/CLI.hs b/src/PostgREST/CLI.hs index 57fcf27ce..cb5a36b82 100644 --- a/src/PostgREST/CLI.hs +++ b/src/PostgREST/CLI.hs @@ -136,6 +136,9 @@ exampleConfigFile = |## Enable in-database configuration |db-config = true | + |## Function for in-database configuration + |## db-pre-config = "postgrest.pre_config" + | |## Extra schemas to add to the search_path of every request |db-extra-search-path = "public" | diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index 3bd48c97a..7b559d4c1 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -80,6 +80,7 @@ data AppConfig = AppConfig , configDbRootSpec :: Maybe QualifiedIdentifier , configDbSchemas :: NonEmpty Text , configDbConfig :: Bool + , configDbPreConfig :: Maybe QualifiedIdentifier , configDbTxAllowOverride :: Bool , configDbTxRollbackAll :: Bool , configDbUri :: Text @@ -144,6 +145,7 @@ toText conf = ,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec) ,("db-schemas", q . T.intercalate "," . toList . configDbSchemas) ,("db-config", T.toLower . show . configDbConfig) + ,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig) ,("db-tx-end", q . showTxEnd) ,("db-uri", q . configDbUri) ,("db-use-legacy-gucs", T.toLower . show . configDbUseLegacyGucs) @@ -240,6 +242,7 @@ parser optPath env dbSettings roleSettings = <*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optValue "db-schemas") (optValue "db-schema")) <*> (fromMaybe True <$> optBool "db-config") + <*> (fmap toQi <$> optString "db-pre-config") <*> parseTxEnd "db-tx-end" snd <*> parseTxEnd "db-tx-end" fst <*> (fromMaybe "postgresql://" <$> optString "db-uri") diff --git a/src/PostgREST/Config/Database.hs b/src/PostgREST/Config/Database.hs index 880b1930e..711fdc6c6 100644 --- a/src/PostgREST/Config/Database.hs +++ b/src/PostgREST/Config/Database.hs @@ -35,6 +35,7 @@ dbSettingsNames :: [Text] dbSettingsNames = (prefix <>) <$> ["db_anon_role" + ,"db_pre_config" ,"db_extra_search_path" ,"db_max_rows" ,"db_plan_enabled" @@ -64,21 +65,21 @@ pgVersionStatement = SQL.Statement sql HE.noParams versionRow sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')" versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text -queryDbSettings :: Bool -> Session [(Text, Text)] -queryDbSettings prepared = - let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in - transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ dbSettingsStatement prepared - --- | Get db settings from the connection role. Global settings will be overridden by database specific settings. --- i.e. Doing: --- ALTER ROLE authenticator IN DATABASE postgres SET jwt_aud = 'val'; --- ALTER ROLE authenticator SET jwt_aud = 'overridden'; --- Will result in jwt_aud = 'overridden' +-- | Query the in-database configuration. The settings have the following priorities: -- --- A setting on the database only will have no effect --- ALTER DATABASE postgres SET jwt_aud = 'xx' -dbSettingsStatement :: Bool -> SQL.Statement [Text] [(Text, Text)] -dbSettingsStatement = SQL.Statement sql (arrayParam HE.text) decodeSettings +-- 1. Role + with database-specific settings: +-- ALTER ROLE authenticator IN DATABASE postgres SET jwt_aud = 'val'; +-- 2. Role + with settings: +-- ALTER ROLE authenticator SET jwt_aud = 'overridden'; +-- 3. pre-config function: +-- CREATE FUNCTION pre_config() .. PERFORM set_config(jwt_aud, 'pre_config_aud'..) +-- +-- The example above will result in jwt_aud = 'val' +-- A setting on the database only will have no effect: ALTER DATABASE postgres SET jwt_aud = 'xx' +queryDbSettings :: Maybe Text -> Bool -> Session [(Text, Text)] +queryDbSettings preConfFunc prepared = + let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in + transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared where sql = [qc| WITH @@ -94,14 +95,26 @@ dbSettingsStatement = SQL.Statement sql (arrayParam HE.text) decodeSettings substr(setting, 1, strpos(setting, '=') - 1) as k, substr(setting, strpos(setting, '=') + 1) as v FROM role_setting + {preConfigF} ) SELECT DISTINCT ON (key) replace(k, '{prefix}', '') AS key, v AS value FROM kv_settings - WHERE k = ANY($1) - ORDER BY key, database DESC; + WHERE k = ANY($1) AND v IS NOT NULL + ORDER BY key, database DESC NULLS LAST; |] + preConfigF = case preConfFunc of + Nothing -> mempty + Just func -> [qc| + UNION + SELECT + null as database, + x as k, + current_setting(x, true) as v + FROM unnest($1) x + JOIN {func}() _ ON TRUE + |]::Text decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text queryRoleSettings :: Bool -> Session RoleSettings diff --git a/src/PostgREST/Workers.hs b/src/PostgREST/Workers.hs index 28c1cf501..f3e98a6d8 100644 --- a/src/PostgREST/Workers.hs +++ b/src/PostgREST/Workers.hs @@ -25,13 +25,17 @@ import Hasql.Connection (acquire) import Network.Socket import Network.Socket.ByteString -import PostgREST.AppState (AppState) -import PostgREST.Config (AppConfig (..), readAppConfig) -import PostgREST.Config.Database (queryDbSettings, queryPgVersion, - queryRoleSettings) -import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion) -import PostgREST.Error (checkIsFatal) -import PostgREST.SchemaCache (querySchemaCache) +import PostgREST.AppState (AppState) +import PostgREST.Config (AppConfig (..), + readAppConfig) +import PostgREST.Config.Database (queryDbSettings, + queryPgVersion, + queryRoleSettings) +import PostgREST.Config.PgVersion (PgVersion (..), + minimumPgVersion) +import PostgREST.Error (checkIsFatal) +import PostgREST.SchemaCache (querySchemaCache) +import PostgREST.SchemaCache.Identifiers (dumpQi) import qualified PostgREST.AppState as AppState @@ -89,8 +93,8 @@ connectionWorker appState = do when configDbChannelEnabled $ AppState.signalListener appState AppState.logWithZTime appState "Connection successful" - -- this could be fail because the connection drops, but the - -- loadSchemaCache will pick the error and retry again + -- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again + -- We cannot retry after it fails immediately, because db-pre-config could have user errors. We just log the error and continue. when configDbConfig $ reReadConfig False appState scStatus <- loadSchemaCache appState case scStatus of @@ -237,7 +241,7 @@ reReadConfig startingUp appState = do AppConfig{..} <- AppState.getConfig appState dbSettings <- if configDbConfig then do - qDbSettings <- AppState.usePool appState $ queryDbSettings configDbPreparedStatements + qDbSettings <- AppState.usePool appState $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements case qDbSettings of Left e -> do AppState.logWithZTime appState diff --git a/test/io/configs/expected/aliases.config b/test/io/configs/expected/aliases.config index 06c03eef6..fedbac8b0 100644 --- a/test/io/configs/expected/aliases.config +++ b/test/io/configs/expected/aliases.config @@ -13,6 +13,7 @@ db-prepared-statements = true db-root-spec = "open_alias" db-schemas = "provided_through_alias" db-config = true +db-pre-config = "" db-tx-end = "commit" db-uri = "postgresql://" db-use-legacy-gucs = true diff --git a/test/io/configs/expected/boolean-numeric.config b/test/io/configs/expected/boolean-numeric.config index 6b0a38483..37abeb035 100644 --- a/test/io/configs/expected/boolean-numeric.config +++ b/test/io/configs/expected/boolean-numeric.config @@ -13,6 +13,7 @@ db-prepared-statements = false db-root-spec = "" db-schemas = "public" db-config = true +db-pre-config = "" db-tx-end = "commit" db-uri = "postgresql://" db-use-legacy-gucs = true diff --git a/test/io/configs/expected/boolean-string.config b/test/io/configs/expected/boolean-string.config index 6b0a38483..37abeb035 100644 --- a/test/io/configs/expected/boolean-string.config +++ b/test/io/configs/expected/boolean-string.config @@ -13,6 +13,7 @@ db-prepared-statements = false db-root-spec = "" db-schemas = "public" db-config = true +db-pre-config = "" db-tx-end = "commit" db-uri = "postgresql://" db-use-legacy-gucs = true diff --git a/test/io/configs/expected/defaults.config b/test/io/configs/expected/defaults.config index 1579c3442..ab914aeac 100644 --- a/test/io/configs/expected/defaults.config +++ b/test/io/configs/expected/defaults.config @@ -13,6 +13,7 @@ db-prepared-statements = true db-root-spec = "" db-schemas = "public" db-config = false +db-pre-config = "" db-tx-end = "commit" db-uri = "postgresql://" db-use-legacy-gucs = true diff --git a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config index 2ca428fbe..dceefd8ae 100644 --- a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config +++ b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config @@ -1,4 +1,4 @@ -db-anon-role = "other" +db-anon-role = "pre_config_role" db-channel = "postgrest" db-channel-enabled = false db-extra-search-path = "public,extensions,other" @@ -13,11 +13,12 @@ db-prepared-statements = false db-root-spec = "other_root" db-schemas = "test,other_tenant1,other_tenant2" db-config = true +db-pre-config = "postgrest.pre_config" db-tx-end = "rollback-allow-override" db-uri = "postgresql://" db-use-legacy-gucs = false jwt-aud = "https://otherexample.org" -jwt-role-claim-key = ".\"other\".\"role\"" +jwt-role-claim-key = ".\"other\".\"pre_config_role\"" jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE" jwt-secret-is-base64 = true log-level = "info" diff --git a/test/io/configs/expected/no-defaults-with-db.config b/test/io/configs/expected/no-defaults-with-db.config index 4799deef6..0676308c4 100644 --- a/test/io/configs/expected/no-defaults-with-db.config +++ b/test/io/configs/expected/no-defaults-with-db.config @@ -13,6 +13,7 @@ db-prepared-statements = false db-root-spec = "root" db-schemas = "test,tenant1,tenant2" db-config = true +db-pre-config = "postgrest.preconf" db-tx-end = "commit-allow-override" db-uri = "postgresql://" db-use-legacy-gucs = false diff --git a/test/io/configs/expected/no-defaults.config b/test/io/configs/expected/no-defaults.config index 9706071e6..2be86049f 100644 --- a/test/io/configs/expected/no-defaults.config +++ b/test/io/configs/expected/no-defaults.config @@ -13,6 +13,7 @@ db-prepared-statements = false db-root-spec = "openapi_v3" db-schemas = "multi,tenant,setup" db-config = false +db-pre-config = "postgrest.pre_config" db-tx-end = "rollback-allow-override" db-uri = "tmp_db" db-use-legacy-gucs = false diff --git a/test/io/configs/expected/types.config b/test/io/configs/expected/types.config index 7d8812f7f..7259e4a43 100644 --- a/test/io/configs/expected/types.config +++ b/test/io/configs/expected/types.config @@ -13,6 +13,7 @@ db-prepared-statements = true db-root-spec = "" db-schemas = "public" db-config = true +db-pre-config = "" db-tx-end = "commit" db-uri = "postgresql://" db-use-legacy-gucs = true diff --git a/test/io/configs/no-defaults-env.yaml b/test/io/configs/no-defaults-env.yaml index fc8061bbf..126075e4d 100644 --- a/test/io/configs/no-defaults-env.yaml +++ b/test/io/configs/no-defaults-env.yaml @@ -15,9 +15,9 @@ PGRST_DB_PRE_REQUEST: please_run_fast PGRST_DB_ROOT_SPEC: openapi_v3 PGRST_DB_SCHEMAS: multi, tenant,setup PGRST_DB_CONFIG: false +PGRST_DB_PRE_CONFIG: "postgrest.pre_config" PGRST_DB_TX_END: rollback-allow-override PGRST_DB_URI: tmp_db -PGRST_DB_EMBED_DEFAULT_JOIN: inner PGRST_DB_USE_LEGACY_GUCS: false PGRST_JWT_AUD: 'https://postgrest.org' PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"' diff --git a/test/io/configs/no-defaults.config b/test/io/configs/no-defaults.config index 70c771760..2a9f73614 100644 --- a/test/io/configs/no-defaults.config +++ b/test/io/configs/no-defaults.config @@ -13,6 +13,7 @@ db-prepared-statements = false db-root-spec = "openapi_v3" db-schemas = "multi, tenant,setup" db-config = false +db-pre-config = "postgrest.pre_config" db-tx-end = "rollback-allow-override" db-uri = "tmp_db" db-use-legacy-gucs = false diff --git a/test/io/db_config.sql b/test/io/db_config.sql index 10a552597..a9f5c147b 100644 --- a/test/io/db_config.sql +++ b/test/io/db_config.sql @@ -9,6 +9,7 @@ ALTER ROLE db_config_authenticator SET pgrst.jwt_secret_is_base64 = 'false'; ALTER ROLE db_config_authenticator SET pgrst.jwt_role_claim_key = '."a"."role"'; ALTER ROLE db_config_authenticator SET pgrst.db_anon_role = 'anonymous'; ALTER ROLE db_config_authenticator SET pgrst.db_tx_end = 'commit-allow-override'; +ALTER ROLE db_config_authenticator SET pgrst.db_pre_config = 'postgrest.preconf'; ALTER ROLE db_config_authenticator SET pgrst.db_schemas = 'test, tenant1, tenant2'; ALTER ROLE db_config_authenticator SET pgrst.db_root_spec = 'root'; ALTER ROLE db_config_authenticator SET pgrst.db_plan_enabled = 'true'; @@ -43,7 +44,7 @@ ALTER ROLE db_config_authenticator SET pgrst.db_pool_timeout = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.db_pool_acquisition_timeout = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.db_pool_max_lifetime = 'ignored'; ALTER ROLE db_config_authenticator SET pgrst.db_pool_max_idletime = 'ignored'; -ALTER ROLE db_config_authenticator SET pgrst.db_config = 'ignored'; +ALTER ROLE db_config_authenticator SET pgrst.db_config = 'true'; -- other authenticator reloadable config options CREATE ROLE other_authenticator LOGIN NOINHERIT; @@ -52,9 +53,6 @@ ALTER ROLE other_authenticator SET pgrst.openapi_server_proxy_uri = 'https://oth ALTER ROLE other_authenticator SET pgrst.raw_media_types = 'application/vnd.pgrst.other-db-config'; ALTER ROLE other_authenticator SET pgrst.jwt_secret = 'ODERREALLYREALLYREALLYREALLYVERYSAFE'; ALTER ROLE other_authenticator SET pgrst.jwt_secret_is_base64 = 'true'; -ALTER ROLE other_authenticator SET pgrst.jwt_role_claim_key = '."other"."role"'; -ALTER ROLE other_authenticator SET pgrst.db_anon_role = 'other'; -ALTER ROLE other_authenticator SET pgrst.db_tx_end = 'rollback-allow-override'; ALTER ROLE other_authenticator SET pgrst.db_schemas = 'test, other_tenant1, other_tenant2'; ALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root'; ALTER ROLE other_authenticator SET pgrst.db_plan_enabled = 'true'; @@ -65,6 +63,32 @@ ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensi ALTER ROLE other_authenticator SET pgrst.openapi_mode = 'disabled'; ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false'; ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent'; +ALTER ROLE other_authenticator SET pgrst.db_pre_config = 'postgrest.pre_config'; + +create schema postgrest; +grant usage on schema postgrest to db_config_authenticator; +grant usage on schema postgrest to other_authenticator; + +-- pre-config hook +create or replace function postgrest.pre_config() +returns void as $$ +begin + if current_user = 'other_authenticator' then + perform + set_config('pgrst.jwt_role_claim_key', '."other"."pre_config_role"', true) + , set_config('pgrst.db_anon_role', 'pre_config_role', true) + , set_config('pgrst.db_schemas', 'will be overriden with the above ALTER ROLE.. db_schemas', true) + , set_config('pgrst.db_tx_end', 'rollback-allow-override', true); + else + null; + end if; +end $$ language plpgsql; + +create or replace function postgrest.preconf() +returns void as $$ +begin + null; +end $$ language plpgsql; -- authenticator used for tests that manipulate statement timeout CREATE ROLE timeout_authenticator LOGIN NOINHERIT; diff --git a/test/spec/SpecHelper.hs b/test/spec/SpecHelper.hs index 8d5bff865..3421db894 100644 --- a/test/spec/SpecHelper.hs +++ b/test/spec/SpecHelper.hs @@ -90,6 +90,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in , configDbRootSpec = Nothing , configDbSchemas = fromList ["test"] , configDbConfig = False + , configDbPreConfig = Nothing , configDbUri = "postgresql://" , configDbUseLegacyGucs = True , configFilePath = Nothing