feat: apply super settings on impersonated roles
If they have GRANT SET ON PARAMETER <setting> TO authenticator
This commit is contained in:
committed by
Steve Chavez
parent
125f10a60f
commit
f7bf2157f3
@@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
+ Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901
|
+ Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901
|
||||||
- #2799, Add timezone in Prefer header - @taimoorzaeem
|
- #2799, Add timezone in Prefer header - @taimoorzaeem
|
||||||
- #3001, Add `statement_timeout` set on functions - @taimoorzaeem
|
- #3001, Add `statement_timeout` set on functions - @taimoorzaeem
|
||||||
|
- #3045, Apply superuser settings on impersonated roles if they have PostgreSQL 15 `GRANT SET ON PARAMETER` privilege - @steve-chavez
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ establishConnection appState =
|
|||||||
reReadConfig :: Bool -> AppState -> IO ()
|
reReadConfig :: Bool -> AppState -> IO ()
|
||||||
reReadConfig startingUp appState = do
|
reReadConfig startingUp appState = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
|
pgVer <- getPgVersion appState
|
||||||
dbSettings <-
|
dbSettings <-
|
||||||
if configDbConfig then do
|
if configDbConfig then do
|
||||||
qDbSettings <- usePool appState $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
|
qDbSettings <- usePool appState $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
|
||||||
@@ -396,7 +397,7 @@ reReadConfig startingUp appState = do
|
|||||||
pure mempty
|
pure mempty
|
||||||
(roleSettings, roleIsolationLvl) <-
|
(roleSettings, roleIsolationLvl) <-
|
||||||
if configDbConfig then do
|
if configDbConfig then do
|
||||||
rSettings <- usePool appState $ queryRoleSettings configDbPreparedStatements
|
rSettings <- usePool appState $ queryRoleSettings pgVer configDbPreparedStatements
|
||||||
case rSettings of
|
case rSettings of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
logWithZTime appState "An error ocurred when trying to query the role settings"
|
logWithZTime appState "An error ocurred when trying to query the role settings"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module PostgREST.Config.Database
|
|||||||
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
|
||||||
@@ -127,8 +127,8 @@ queryDbSettings preConfFunc prepared =
|
|||||||
|]::Text
|
|]::Text
|
||||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||||
|
|
||||||
queryRoleSettings :: Bool -> Session (RoleSettings, RoleIsolationLvl)
|
queryRoleSettings :: PgVersion -> Bool -> Session (RoleSettings, RoleIsolationLvl)
|
||||||
queryRoleSettings prepared =
|
queryRoleSettings pgVer prepared =
|
||||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
|
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
|
||||||
where
|
where
|
||||||
@@ -157,7 +157,10 @@ queryRoleSettings prepared =
|
|||||||
i.value as iso_lvl,
|
i.value as iso_lvl,
|
||||||
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
|
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
|
||||||
from kv_settings kv
|
from kv_settings kv
|
||||||
join pg_settings ps on ps.name = kv.key and ps.context = 'user'
|
join pg_settings ps on ps.name = kv.key |] <>
|
||||||
|
(if pgVer >= pgVersion150
|
||||||
|
then "and (ps.context = 'user' or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')) "
|
||||||
|
else "and ps.context = 'user' ") <> [q|
|
||||||
left join iso_setting i on i.rolname = kv.rolname
|
left join iso_setting i on i.rolname = kv.rolname
|
||||||
group by kv.rolname, i.value;
|
group by kv.rolname, i.value;
|
||||||
|]
|
|]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ module PostgREST.Config.PgVersion
|
|||||||
, pgVersion121
|
, pgVersion121
|
||||||
, pgVersion130
|
, pgVersion130
|
||||||
, pgVersion140
|
, pgVersion140
|
||||||
|
, pgVersion150
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -62,3 +63,6 @@ pgVersion130 = PgVersion 130000 "13.0"
|
|||||||
|
|
||||||
pgVersion140 :: PgVersion
|
pgVersion140 :: PgVersion
|
||||||
pgVersion140 = PgVersion 140000 "14.0"
|
pgVersion140 = PgVersion 140000 "14.0"
|
||||||
|
|
||||||
|
pgVersion150 :: PgVersion
|
||||||
|
pgVersion150 = PgVersion 150000 "15.0"
|
||||||
|
|||||||
@@ -233,12 +233,14 @@ optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
|
|||||||
shouldRollback =
|
shouldRollback =
|
||||||
preferTransaction == Just Rollback
|
preferTransaction == Just Rollback
|
||||||
|
|
||||||
-- | Runs local (transaction scoped) GUCs for every request.
|
-- | Set transaction scoped settings
|
||||||
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> [(ByteString, ByteString)] ->
|
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> [(ByteString, ByteString)] ->
|
||||||
ApiRequest -> Maybe Text -> DbHandler ()
|
ApiRequest -> Maybe Text -> DbHandler ()
|
||||||
setPgLocals AppConfig{..} claims role roleSettings ApiRequest{..} tout = lift $
|
setPgLocals AppConfig{..} claims role roleSettings ApiRequest{..} tout = lift $
|
||||||
SQL.statement mempty $ SQL.dynamicallyParameterized
|
SQL.statement mempty $ SQL.dynamicallyParameterized
|
||||||
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ roleSettingsSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ timeoutSql ++ appSettingsSql))
|
-- To ensure `GRANT SET ON PARAMETER <superuser_setting> TO authenticator` works, the role settings must be set before the impersonated role.
|
||||||
|
-- Otherwise the GRANT SET would have to be applied to the impersonated role. See https://github.com/PostgREST/postgrest/issues/3045
|
||||||
|
("select " <> intercalateSnippet ", " (searchPathSql : roleSettingsSql ++ roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ timezoneSql ++ timeoutSql ++ appSettingsSql))
|
||||||
HD.noResult configDbPreparedStatements
|
HD.noResult configDbPreparedStatements
|
||||||
where
|
where
|
||||||
methodSql = setConfigWithConstantName ("request.method", iMethod)
|
methodSql = setConfigWithConstantName ("request.method", iMethod)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
-- \ir big_schema.sql big schema test currently skipped, see test_io.py
|
-- \ir big_schema.sql big schema test currently skipped, see test_io.py
|
||||||
\ir db_config.sql
|
\ir db_config.sql
|
||||||
|
|
||||||
|
set check_function_bodies = false; -- to allow conditionals based on the pg version
|
||||||
set search_path to public;
|
set search_path to public;
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
@@ -18,6 +19,13 @@ CREATE ROLE postgrest_test_w_superuser_settings;
|
|||||||
alter role postgrest_test_w_superuser_settings set log_min_duration_statement = 1;
|
alter role postgrest_test_w_superuser_settings set log_min_duration_statement = 1;
|
||||||
alter role postgrest_test_w_superuser_settings set log_min_messages = 'fatal';
|
alter role postgrest_test_w_superuser_settings set log_min_messages = 'fatal';
|
||||||
|
|
||||||
|
DO $do$BEGIN
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 150000) THEN
|
||||||
|
ALTER ROLE postgrest_test_w_superuser_settings SET log_min_duration_sample = 12345;
|
||||||
|
GRANT SET ON PARAMETER log_min_duration_sample to postgrest_test_authenticator;
|
||||||
|
END IF;
|
||||||
|
END$do$;
|
||||||
|
|
||||||
GRANT
|
GRANT
|
||||||
postgrest_test_anonymous, postgrest_test_author,
|
postgrest_test_anonymous, postgrest_test_author,
|
||||||
postgrest_test_serializable, postgrest_test_repeatable_read,
|
postgrest_test_serializable, postgrest_test_repeatable_read,
|
||||||
@@ -186,3 +194,7 @@ $$ language sql set statement_timeout = '1s';
|
|||||||
create or replace function four_sec_timeout() returns void as $$
|
create or replace function four_sec_timeout() returns void as $$
|
||||||
select pg_sleep(3);
|
select pg_sleep(3);
|
||||||
$$ language sql set statement_timeout = '4s';
|
$$ language sql set statement_timeout = '4s';
|
||||||
|
|
||||||
|
create function get_postgres_version() returns int as $$
|
||||||
|
select current_setting('server_version_num')::int;
|
||||||
|
$$ language sql;
|
||||||
|
|||||||
@@ -1065,6 +1065,24 @@ def test_succeed_w_role_having_superuser_settings(defaultenv):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_granted_superuser_setting(defaultenv):
|
||||||
|
"Should succeed when the impersonated role has granted superuser settings"
|
||||||
|
|
||||||
|
env = {**defaultenv, "PGRST_DB_CONFIG": "true", "PGRST_JWT_SECRET": SECRET}
|
||||||
|
|
||||||
|
with run(stdin=SECRET.encode(), env=env) as postgrest:
|
||||||
|
response_ver = postgrest.session.get("/rpc/get_postgres_version")
|
||||||
|
pg_ver = eval(response_ver.text)
|
||||||
|
if pg_ver >= 150000:
|
||||||
|
headers = jwtauthheader(
|
||||||
|
{"role": "postgrest_test_w_superuser_settings"}, SECRET
|
||||||
|
)
|
||||||
|
response = postgrest.session.get(
|
||||||
|
"/rpc/get_guc_value?name=log_min_duration_sample", headers=headers
|
||||||
|
)
|
||||||
|
assert response.text == '"12345ms"'
|
||||||
|
|
||||||
|
|
||||||
def test_fail_with_invalid_dbname_and_automatic_recovery_disabled(defaultenv):
|
def test_fail_with_invalid_dbname_and_automatic_recovery_disabled(defaultenv):
|
||||||
"Should fail without retries when automatic recovery is disabled and dbname is invalid"
|
"Should fail without retries when automatic recovery is disabled and dbname is invalid"
|
||||||
dbname = "INVALID"
|
dbname = "INVALID"
|
||||||
|
|||||||
Reference in New Issue
Block a user