Skip to content

Commit 20880c7

Browse files
committed
remove(feat): fail schema cache lookup with invalid db-schemas config
Closes #4364 by reverting 86c3257 As discussed on #4364, 86c3257 generates several problems: 1)We diverge from database as source of truth because once a schema is dropped, it's not automatically removed from `db-schemas` and then postgREST fails. 2)We diverge from PostgreSQL `search_path` lenient behavior: ```sql set search_path to anything, here, even, nonexistent; -- this doesn't fail ``` 3)schema-based multitenancy becomes more complicated because of 1. There are no noticeable gains from 86c3257: - No gains in performance for the schema cache queries. - No user report about it ever being a problem. In fact the leniency was a feature.
1 parent 0f7ac1b commit 20880c7

File tree

6 files changed

+104
-30
lines changed

6 files changed

+104
-30
lines changed

src/PostgREST/Query/SqlFragment.hs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -595,8 +595,14 @@ handlerF rout = \case
595595
NoAgg -> "''::text"
596596

597597
schemaDescription :: Text -> SQL.Snippet
598-
schemaDescription schema =
599-
"SELECT pg_catalog.obj_description(" <> encoded <> "::regnamespace, 'pg_namespace')"
598+
schemaDescription schema = SQL.sql (encodeUtf8 [trimming|
599+
SELECT
600+
description
601+
FROM
602+
pg_namespace n
603+
left join pg_description d on d.objoid = n.oid
604+
WHERE
605+
n.nspname = |]) <> encoded
600606
where
601607
encoded = SQL.encoderAndParam (HE.nonNullable HE.unknown) $ encodeUtf8 schema
602608

@@ -608,7 +614,7 @@ accessibleTables schema = SQL.sql (encodeUtf8 [trimming|
608614
FROM pg_class c
609615
JOIN pg_namespace n ON n.oid = c.relnamespace
610616
WHERE c.relkind IN ('v','r','m','f','p')
611-
AND c.relnamespace = |]) <> encodedSchema <> "::regnamespace " <> SQL.sql (encodeUtf8 [trimming|
617+
AND n.nspname = |]) <> encodedSchema <> " " <> SQL.sql (encodeUtf8 [trimming|
612618
AND (
613619
pg_has_role(c.relowner, 'USAGE')
614620
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
@@ -620,7 +626,7 @@ accessibleTables schema = SQL.sql (encodeUtf8 [trimming|
620626
encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema
621627

622628
accessibleFuncs :: Text -> SQL.Snippet
623-
accessibleFuncs schema = baseFuncSqlQuery <> "AND p.pronamespace = " <> encodedSchema <> "::regnamespace"
629+
accessibleFuncs schema = baseFuncSqlQuery <> "AND pn.nspname = " <> encodedSchema
624630
where
625631
encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema
626632

src/PostgREST/SchemaCache.hs

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ import NeatInterpolation (trimming)
4242
import PostgREST.Config (AppConfig (..))
4343
import PostgREST.Config.Database (TimezoneNames,
4444
toIsolationLevel)
45-
import PostgREST.Query.SqlFragment (escapeIdent)
4645
import PostgREST.SchemaCache.Identifiers (FieldName,
4746
QualifiedIdentifier (..),
4847
RelIdentifier (..),
@@ -357,7 +356,7 @@ allFunctions :: Bool -> SQL.Statement AppConfig RoutineMap
357356
allFunctions = SQL.Statement funcsSqlQuery params decodeFuncs
358357
where
359358
params =
360-
(map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>
359+
(toList . configDbSchemas >$< arrayParam HE.text) <>
361360
(configDbHoistedTxSettings >$< arrayParam HE.text)
362361

363362
baseTypesCte :: Text
@@ -458,7 +457,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
458457
) func_settings ON TRUE
459458
WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true)
460459
AND prokind = 'f'
461-
AND p.pronamespace = ANY($$1::regnamespace[]) |]
460+
AND pn.nspname = ANY($$1) |]
462461
{-
463462
Adds M2O and O2O relationships for views to tables, tables to views, and views to views. The example below is taken from the test fixtures, but the views names/colnames were modified.
464463
@@ -569,7 +568,7 @@ addViewPrimaryKeys tabs keyDeps =
569568
allTables :: Bool -> SQL.Statement AppConfig TablesMap
570569
allTables = SQL.Statement tablesSqlQuery params decodeTables
571570
where
572-
params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
571+
params = toList . configDbSchemas >$< arrayParam HE.text
573572

574573
-- | Gets tables with their PK cols
575574
tablesSqlQuery :: SqlQuery
@@ -621,6 +620,8 @@ tablesSqlQuery =
621620
ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
622621
JOIN pg_class c
623622
ON a.attrelid = c.oid
623+
JOIN pg_namespace nc
624+
ON c.relnamespace = nc.oid
624625
JOIN pg_type t
625626
ON a.atttypid = t.oid
626627
LEFT JOIN base_types bt
@@ -632,7 +633,7 @@ tablesSqlQuery =
632633
AND a.attnum > 0
633634
AND NOT a.attisdropped
634635
AND c.relkind in ('r', 'v', 'f', 'm', 'p')
635-
AND c.relnamespace = ANY($$1::regnamespace[])
636+
AND nc.nspname = ANY($$1)
636637
),
637638
columns_agg AS (
638639
SELECT
@@ -812,8 +813,8 @@ allViewsKeyDependencies =
812813
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
813814
where
814815
params =
815-
(map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>
816-
(map escapeIdent . toList . configDbExtraSearchPath >$< arrayParam HE.text)
816+
(toList . configDbSchemas >$< arrayParam HE.text) <>
817+
(configDbExtraSearchPath >$< arrayParam HE.text)
817818
sql = encodeUtf8 [trimming|
818819
with recursive
819820
pks_fks as (
@@ -844,18 +845,17 @@ allViewsKeyDependencies =
844845
views as (
845846
select
846847
c.oid as view_id,
847-
c.relnamespace as view_schema_id,
848848
n.nspname as view_schema,
849849
c.relname as view_name,
850850
r.ev_action as view_definition
851851
from pg_class c
852852
join pg_namespace n on n.oid = c.relnamespace
853853
join pg_rewrite r on r.ev_class = c.oid
854-
where c.relkind in ('v', 'm') and c.relnamespace = ANY($$1::regnamespace[] || $$2::regnamespace[])
854+
where c.relkind in ('v', 'm') and n.nspname = ANY($$1 || $$2)
855855
),
856856
transform_json as (
857857
select
858-
view_id, view_schema_id, view_schema, view_name,
858+
view_id, view_schema, view_name,
859859
-- the following formatting is without indentation on purpose
860860
-- to allow simple diffs, with less whitespace noise
861861
replace(
@@ -935,31 +935,30 @@ allViewsKeyDependencies =
935935
),
936936
target_entries as(
937937
select
938-
view_id, view_schema_id, view_schema, view_name,
938+
view_id, view_schema, view_name,
939939
json_array_elements(view_definition->0->'targetList') as entry
940940
from transform_json
941941
),
942942
results as(
943943
select
944-
view_id, view_schema_id, view_schema, view_name,
944+
view_id, view_schema, view_name,
945945
(entry->>'resno')::int as view_column,
946946
(entry->>'resorigtbl')::oid as resorigtbl,
947947
(entry->>'resorigcol')::int as resorigcol
948948
from target_entries
949949
),
950950
-- CYCLE detection according to PG docs: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CYCLE
951951
-- Can be replaced with CYCLE clause once PG v13 is EOL.
952-
recursion(view_id, view_schema_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as(
952+
recursion(view_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as(
953953
select
954954
r.*,
955955
false,
956956
ARRAY[resorigtbl]
957957
from results r
958-
where view_schema_id = ANY ($$1::regnamespace[])
958+
where view_schema = ANY ($$1)
959959
union all
960960
select
961961
view.view_id,
962-
view.view_schema_id,
963962
view.view_schema,
964963
view.view_name,
965964
view.view_column,
@@ -1018,7 +1017,7 @@ mediaHandlers :: Bool -> SQL.Statement AppConfig MediaHandlerMap
10181017
mediaHandlers =
10191018
SQL.Statement sql params decodeMediaHandlers
10201019
where
1021-
params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
1020+
params = toList . configDbSchemas >$< arrayParam HE.text
10221021
sql = encodeUtf8 [trimming|
10231022
with
10241023
all_relations as (
@@ -1059,7 +1058,7 @@ mediaHandlers =
10591058
join pg_type arg_name on arg_name.oid = proc.proargtypes[0]
10601059
join pg_namespace arg_schema on arg_schema.oid = arg_name.typnamespace
10611060
where
1062-
proc.pronamespace = ANY($$1::regnamespace[]) and
1061+
proc_schema.nspname = ANY($$1) and
10631062
proc.pronargs = 1 and
10641063
arg_name.oid in (select reltype from all_relations)
10651064
union
@@ -1075,7 +1074,7 @@ mediaHandlers =
10751074
join media_types mtype on proc.prorettype = mtype.oid
10761075
join pg_namespace typ_sch on typ_sch.oid = mtype.typnamespace
10771076
where
1078-
proc.pronamespace = ANY($$1::regnamespace[]) and NOT proretset
1077+
pro_sch.nspname = ANY($$1) and NOT proretset
10791078
and prokind = 'f'|]
10801079

10811080
decodeMediaHandlers :: HD.Result MediaHandlerMap

test/io/test_io.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,8 +1051,10 @@ def drain_stdout(proc):
10511051
)
10521052
infinite_recursion_5xx_regx = r'.+: WITH pgrst_source AS.+SELECT "public"\."infinite_recursion"\.\* FROM "public"\."infinite_recursion".+_postgrest_t'
10531053
root_tables_regx = r".+: SELECT n.nspname AS table_schema, .+ FROM pg_class c .+ ORDER BY table_schema, table_name"
1054-
root_procs_regx = r".+: WITH base_types AS \(.+\) SELECT pn.nspname AS proc_schema, .+ FROM pg_proc p.+AND p.pronamespace = \$1::regnamespace"
1055-
root_descr_regx = r".+: SELECT pg_catalog\.obj_description\(\$1::regnamespace, 'pg_namespace'\)"
1054+
root_procs_regx = r".+: WITH base_types AS \(.+\) SELECT pn.nspname AS proc_schema, .+ FROM pg_proc p.+AND pn.nspname = \$1"
1055+
root_descr_regx = (
1056+
r".+: SELECT.+description.+FROM.+pg_namespace n.+WHERE.+n.nspname =\$1"
1057+
)
10561058
set_config_regx = (
10571059
r".+: select set_config\('search_path', \$1, true\), set_config\("
10581060
)
@@ -2010,23 +2012,29 @@ def test_allow_configs_to_be_set_to_empty(defaultenv):
20102012
assert response.status_code == 200
20112013

20122014

2013-
def test_schema_cache_error_observation(defaultenv):
2015+
def test_schema_cache_error_observation(defaultenv, metapostgrest):
20142016
"schema cache error observation should be logged with invalid db-schemas or db-extra-search-path"
20152017

2018+
role = "timeout_authenticator"
2019+
20162020
env = {
2017-
**defaultenv,
2018-
"PGRST_DB_EXTRA_SEARCH_PATH": "x",
2021+
**defaultenv,
2022+
"PGUSER": role,
2023+
"PGRST_DB_ANON_ROLE": role,
2024+
"PGRST_INTERNAL_SCHEMA_CACHE_SLEEP": "500",
20192025
}
20202026

20212027
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
20222028
# TODO: postgrest should exit here, instead it keeps retrying
20232029
# exitCode = wait_until_exit(postgrest)
20242030
# assert exitCode == 1
2031+
set_statement_timeout(metapostgrest, role, 400)
2032+
2033+
output = postgrest.read_stdout(nlines=10)
20252034

2026-
output = postgrest.read_stdout(nlines=9)
20272035
assert (
2028-
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
2029-
in output[7]
2036+
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=public"
2037+
in line for line in output
20302038
)
20312039

20322040

test/spec/Feature/Query/ErrorSpec.hs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,59 @@ import Test.Hspec.Wai.JSON
1010
import Protolude hiding (get)
1111
import SpecHelper
1212

13+
nonExistentSchema :: SpecWith ((), Application)
14+
nonExistentSchema = do
15+
describe "Non existent api schema" $ do
16+
it "succeeds when requesting root path" $
17+
get "/" `shouldRespondWith` 200
18+
19+
it "gives 404 when requesting a nonexistent table in this nonexistent schema" $
20+
get "/nonexistent_table" `shouldRespondWith` 404
21+
22+
describe "Non existent URL" $ do
23+
it "gives 404 on a single nested route" $
24+
get "/projects/nested" `shouldRespondWith` 404
25+
26+
it "gives 404 on a double nested route" $
27+
get "/projects/nested/double" `shouldRespondWith` 404
28+
29+
describe "Unsupported HTTP methods" $ do
30+
it "should return 405 for CONNECT method" $
31+
request methodConnect "/"
32+
[]
33+
""
34+
`shouldRespondWith`
35+
[json|
36+
{"hint": null,
37+
"details": null,
38+
"code": "PGRST117",
39+
"message":"Unsupported HTTP method: CONNECT"}|]
40+
{ matchStatus = 405 }
41+
42+
it "should return 405 for TRACE method" $
43+
request methodTrace "/"
44+
[]
45+
""
46+
`shouldRespondWith`
47+
[json|
48+
{"hint": null,
49+
"details": null,
50+
"code": "PGRST117",
51+
"message":"Unsupported HTTP method: TRACE"}|]
52+
{ matchStatus = 405 }
53+
54+
it "should return 405 for OTHER method" $
55+
request "OTHER" "/"
56+
[]
57+
""
58+
`shouldRespondWith`
59+
[json|
60+
{"hint": null,
61+
"details": null,
62+
"code": "PGRST117",
63+
"message":"Unsupported HTTP method: OTHER"}|]
64+
{ matchStatus = 405 }
65+
1366
pgErrorCodeMapping :: SpecWith ((), Application)
1467
pgErrorCodeMapping = do
1568
describe "PostreSQL error code mappings" $ do

test/spec/Main.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ main = do
126126

127127
extraSearchPathApp = appDbs testCfgExtraSearchPath
128128
unicodeApp = appDbs testUnicodeCfg
129+
nonexistentSchemaApp = appDbs testNonexistentSchemaCfg
129130
multipleSchemaApp = appDbs testMultipleSchemaCfg
130131
ignorePrivOpenApi = appDbs testIgnorePrivOpenApiCfg
131132

@@ -221,6 +222,10 @@ main = do
221222
parallel $ before asymJwkSetApp $
222223
describe "Feature.Auth.AsymmetricJwtSpec" Feature.Auth.AsymmetricJwtSpec.spec
223224

225+
-- this test runs with a nonexistent db-schema
226+
parallel $ before nonexistentSchemaApp $
227+
describe "Feature.Query.NonExistentSchemaErrorSpec" Feature.Query.ErrorSpec.nonExistentSchema
228+
224229
-- this test runs with an extra search path
225230
parallel $ before extraSearchPathApp $ do
226231
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec

test/spec/SpecHelper.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ testCfgAsymJWKSet =
238238
, configJWKS = rightToMaybe $ parseSecret secret
239239
}
240240

241+
testNonexistentSchemaCfg :: AppConfig
242+
testNonexistentSchemaCfg = baseCfg { configDbSchemas = fromList ["nonexistent"] }
243+
241244
testCfgExtraSearchPath :: AppConfig
242245
testCfgExtraSearchPath = baseCfg { configDbExtraSearchPath = ["public", "extensions", "EXTRA \"@/\\#~_-"] }
243246

0 commit comments

Comments
 (0)