feat: add pre-config function

Allows using the in-database configuration without SUPERUSER
This commit is contained in:
steve-chavez
2023-05-28 10:21:13 -05:00
committed by Steve Chavez
parent 8a3686d86b
commit 14be3fb671
17 changed files with 93 additions and 33 deletions
+3
View File
@@ -11,6 +11,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
+ New option `db-pool-max-idletime` (default 30s). + New option `db-pool-max-idletime` (default 30s).
+ This is equivalent to the old option `db-pool-timeout` of PostgREST 10.0.0. + This is equivalent to the old option `db-pool-timeout` of PostgREST 10.0.0.
+ A config alias for `db-pool-timeout` is included. + 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 ## [11.0.1] - 2023-04-27
+3
View File
@@ -136,6 +136,9 @@ exampleConfigFile =
|## Enable in-database configuration |## Enable in-database configuration
|db-config = true |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 |## Extra schemas to add to the search_path of every request
|db-extra-search-path = "public" |db-extra-search-path = "public"
| |
+3
View File
@@ -80,6 +80,7 @@ data AppConfig = AppConfig
, configDbRootSpec :: Maybe QualifiedIdentifier , configDbRootSpec :: Maybe QualifiedIdentifier
, configDbSchemas :: NonEmpty Text , configDbSchemas :: NonEmpty Text
, configDbConfig :: Bool , configDbConfig :: Bool
, configDbPreConfig :: Maybe QualifiedIdentifier
, configDbTxAllowOverride :: Bool , configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool , configDbTxRollbackAll :: Bool
, configDbUri :: Text , configDbUri :: Text
@@ -144,6 +145,7 @@ toText conf =
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec) ,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas) ,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
,("db-config", T.toLower . show . configDbConfig) ,("db-config", T.toLower . show . configDbConfig)
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
,("db-tx-end", q . showTxEnd) ,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri) ,("db-uri", q . configDbUri)
,("db-use-legacy-gucs", T.toLower . show . configDbUseLegacyGucs) ,("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") <*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optValue "db-schemas")
(optValue "db-schema")) (optValue "db-schema"))
<*> (fromMaybe True <$> optBool "db-config") <*> (fromMaybe True <$> optBool "db-config")
<*> (fmap toQi <$> optString "db-pre-config")
<*> parseTxEnd "db-tx-end" snd <*> parseTxEnd "db-tx-end" snd
<*> parseTxEnd "db-tx-end" fst <*> parseTxEnd "db-tx-end" fst
<*> (fromMaybe "postgresql://" <$> optString "db-uri") <*> (fromMaybe "postgresql://" <$> optString "db-uri")
+29 -16
View File
@@ -35,6 +35,7 @@ dbSettingsNames :: [Text]
dbSettingsNames = dbSettingsNames =
(prefix <>) <$> (prefix <>) <$>
["db_anon_role" ["db_anon_role"
,"db_pre_config"
,"db_extra_search_path" ,"db_extra_search_path"
,"db_max_rows" ,"db_max_rows"
,"db_plan_enabled" ,"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')" sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text
queryDbSettings :: Bool -> Session [(Text, Text)] -- | Query the in-database configuration. The settings have the following priorities:
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 <prefix>jwt_aud = 'val';
-- ALTER ROLE authenticator SET <prefix>jwt_aud = 'overridden';
-- Will result in <prefix>jwt_aud = 'overridden'
-- --
-- A setting on the database only will have no effect -- 1. Role + with database-specific settings:
-- ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx' -- ALTER ROLE authenticator IN DATABASE postgres SET <prefix>jwt_aud = 'val';
dbSettingsStatement :: Bool -> SQL.Statement [Text] [(Text, Text)] -- 2. Role + with settings:
dbSettingsStatement = SQL.Statement sql (arrayParam HE.text) decodeSettings -- ALTER ROLE authenticator SET <prefix>jwt_aud = 'overridden';
-- 3. pre-config function:
-- CREATE FUNCTION pre_config() .. PERFORM set_config(<prefix>jwt_aud, 'pre_config_aud'..)
--
-- The example above will result in <prefix>jwt_aud = 'val'
-- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>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 where
sql = [qc| sql = [qc|
WITH WITH
@@ -94,14 +95,26 @@ dbSettingsStatement = SQL.Statement sql (arrayParam HE.text) decodeSettings
substr(setting, 1, strpos(setting, '=') - 1) as k, substr(setting, 1, strpos(setting, '=') - 1) as k,
substr(setting, strpos(setting, '=') + 1) as v substr(setting, strpos(setting, '=') + 1) as v
FROM role_setting FROM role_setting
{preConfigF}
) )
SELECT DISTINCT ON (key) SELECT DISTINCT ON (key)
replace(k, '{prefix}', '') AS key, replace(k, '{prefix}', '') AS key,
v AS value v AS value
FROM kv_settings FROM kv_settings
WHERE k = ANY($1) WHERE k = ANY($1) AND v IS NOT NULL
ORDER BY key, database DESC; 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 decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
queryRoleSettings :: Bool -> Session RoleSettings queryRoleSettings :: Bool -> Session RoleSettings
+14 -10
View File
@@ -25,13 +25,17 @@ import Hasql.Connection (acquire)
import Network.Socket import Network.Socket
import Network.Socket.ByteString import Network.Socket.ByteString
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Config (AppConfig (..), readAppConfig) import PostgREST.Config (AppConfig (..),
import PostgREST.Config.Database (queryDbSettings, queryPgVersion, readAppConfig)
queryRoleSettings) import PostgREST.Config.Database (queryDbSettings,
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion) queryPgVersion,
import PostgREST.Error (checkIsFatal) queryRoleSettings)
import PostgREST.SchemaCache (querySchemaCache) 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 import qualified PostgREST.AppState as AppState
@@ -89,8 +93,8 @@ connectionWorker appState = do
when configDbChannelEnabled $ when configDbChannelEnabled $
AppState.signalListener appState AppState.signalListener appState
AppState.logWithZTime appState "Connection successful" AppState.logWithZTime appState "Connection successful"
-- this could be fail because the connection drops, but the -- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again
-- 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 when configDbConfig $ reReadConfig False appState
scStatus <- loadSchemaCache appState scStatus <- loadSchemaCache appState
case scStatus of case scStatus of
@@ -237,7 +241,7 @@ reReadConfig startingUp appState = do
AppConfig{..} <- AppState.getConfig appState AppConfig{..} <- AppState.getConfig appState
dbSettings <- dbSettings <-
if configDbConfig then do if configDbConfig then do
qDbSettings <- AppState.usePool appState $ queryDbSettings configDbPreparedStatements qDbSettings <- AppState.usePool appState $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
case qDbSettings of case qDbSettings of
Left e -> do Left e -> do
AppState.logWithZTime appState AppState.logWithZTime appState
+1
View File
@@ -13,6 +13,7 @@ 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-config = true db-config = true
db-pre-config = ""
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "postgresql://" db-uri = "postgresql://"
db-use-legacy-gucs = true db-use-legacy-gucs = true
@@ -13,6 +13,7 @@ db-prepared-statements = false
db-root-spec = "" db-root-spec = ""
db-schemas = "public" db-schemas = "public"
db-config = true db-config = true
db-pre-config = ""
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "postgresql://" db-uri = "postgresql://"
db-use-legacy-gucs = true db-use-legacy-gucs = true
@@ -13,6 +13,7 @@ db-prepared-statements = false
db-root-spec = "" db-root-spec = ""
db-schemas = "public" db-schemas = "public"
db-config = true db-config = true
db-pre-config = ""
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "postgresql://" db-uri = "postgresql://"
db-use-legacy-gucs = true db-use-legacy-gucs = true
+1
View File
@@ -13,6 +13,7 @@ db-prepared-statements = true
db-root-spec = "" db-root-spec = ""
db-schemas = "public" db-schemas = "public"
db-config = false db-config = false
db-pre-config = ""
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "postgresql://" db-uri = "postgresql://"
db-use-legacy-gucs = true db-use-legacy-gucs = true
@@ -1,4 +1,4 @@
db-anon-role = "other" db-anon-role = "pre_config_role"
db-channel = "postgrest" db-channel = "postgrest"
db-channel-enabled = false db-channel-enabled = false
db-extra-search-path = "public,extensions,other" db-extra-search-path = "public,extensions,other"
@@ -13,11 +13,12 @@ db-prepared-statements = false
db-root-spec = "other_root" db-root-spec = "other_root"
db-schemas = "test,other_tenant1,other_tenant2" db-schemas = "test,other_tenant1,other_tenant2"
db-config = true db-config = true
db-pre-config = "postgrest.pre_config"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "postgresql://" db-uri = "postgresql://"
db-use-legacy-gucs = false db-use-legacy-gucs = false
jwt-aud = "https://otherexample.org" jwt-aud = "https://otherexample.org"
jwt-role-claim-key = ".\"other\".\"role\"" jwt-role-claim-key = ".\"other\".\"pre_config_role\""
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE" jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
jwt-secret-is-base64 = true jwt-secret-is-base64 = true
log-level = "info" log-level = "info"
@@ -13,6 +13,7 @@ db-prepared-statements = false
db-root-spec = "root" db-root-spec = "root"
db-schemas = "test,tenant1,tenant2" db-schemas = "test,tenant1,tenant2"
db-config = true db-config = true
db-pre-config = "postgrest.preconf"
db-tx-end = "commit-allow-override" db-tx-end = "commit-allow-override"
db-uri = "postgresql://" db-uri = "postgresql://"
db-use-legacy-gucs = false db-use-legacy-gucs = false
@@ -13,6 +13,7 @@ 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-config = false db-config = false
db-pre-config = "postgrest.pre_config"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "tmp_db" db-uri = "tmp_db"
db-use-legacy-gucs = false db-use-legacy-gucs = false
+1
View File
@@ -13,6 +13,7 @@ db-prepared-statements = true
db-root-spec = "" db-root-spec = ""
db-schemas = "public" db-schemas = "public"
db-config = true db-config = true
db-pre-config = ""
db-tx-end = "commit" db-tx-end = "commit"
db-uri = "postgresql://" db-uri = "postgresql://"
db-use-legacy-gucs = true db-use-legacy-gucs = true
+1 -1
View File
@@ -15,9 +15,9 @@ 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_CONFIG: false PGRST_DB_CONFIG: false
PGRST_DB_PRE_CONFIG: "postgrest.pre_config"
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_DB_EMBED_DEFAULT_JOIN: inner
PGRST_DB_USE_LEGACY_GUCS: false PGRST_DB_USE_LEGACY_GUCS: false
PGRST_JWT_AUD: 'https://postgrest.org' PGRST_JWT_AUD: 'https://postgrest.org'
PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"' PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
+1
View File
@@ -13,6 +13,7 @@ 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-config = false db-config = false
db-pre-config = "postgrest.pre_config"
db-tx-end = "rollback-allow-override" db-tx-end = "rollback-allow-override"
db-uri = "tmp_db" db-uri = "tmp_db"
db-use-legacy-gucs = false db-use-legacy-gucs = false
+28 -4
View File
@@ -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.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_anon_role = 'anonymous';
ALTER ROLE db_config_authenticator SET pgrst.db_tx_end = 'commit-allow-override'; 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_schemas = 'test, tenant1, tenant2';
ALTER ROLE db_config_authenticator SET pgrst.db_root_spec = 'root'; ALTER ROLE db_config_authenticator SET pgrst.db_root_spec = 'root';
ALTER ROLE db_config_authenticator SET pgrst.db_plan_enabled = 'true'; 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_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_lifetime = 'ignored';
ALTER ROLE db_config_authenticator SET pgrst.db_pool_max_idletime = '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 -- other authenticator reloadable config options
CREATE ROLE other_authenticator LOGIN NOINHERIT; 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.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 = 'ODERREALLYREALLYREALLYREALLYVERYSAFE';
ALTER ROLE other_authenticator SET pgrst.jwt_secret_is_base64 = 'true'; 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_schemas = 'test, other_tenant1, other_tenant2';
ALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root'; ALTER ROLE other_authenticator SET pgrst.db_root_spec = 'other_root';
ALTER ROLE other_authenticator SET pgrst.db_plan_enabled = 'true'; 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_mode = 'disabled';
ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false'; 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.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 -- authenticator used for tests that manipulate statement timeout
CREATE ROLE timeout_authenticator LOGIN NOINHERIT; CREATE ROLE timeout_authenticator LOGIN NOINHERIT;
+1
View File
@@ -90,6 +90,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbRootSpec = Nothing , configDbRootSpec = Nothing
, configDbSchemas = fromList ["test"] , configDbSchemas = fromList ["test"]
, configDbConfig = False , configDbConfig = False
, configDbPreConfig = Nothing
, configDbUri = "postgresql://" , configDbUri = "postgresql://"
, configDbUseLegacyGucs = True , configDbUseLegacyGucs = True
, configFilePath = Nothing , configFilePath = Nothing