Skip to content

Commit b6863f1

Browse files
feat: Fail schema cache lookup with invalid db-schemas config
Previously, we'd silently report "200 OK" on the root endpoint, but would never return any endpoints from the schema cache. Now the schema cache query fails because of the ::regnamespace cast.
1 parent 724b6c5 commit b6863f1

File tree

6 files changed

+33
-88
lines changed

6 files changed

+33
-88
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
2525
- #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther
2626
- #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7
2727
- #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla
28+
- #3644, Fail schema cache lookup with invalid db-schemas config - @wolfgangwalther
29+
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
2830

2931
## [12.2.1] - 2024-06-27
3032

src/PostgREST/Query/SqlFragment.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,10 @@ pgBuildArrayLiteral vals =
157157

158158
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
159159
pgFmtIdent :: Text -> SQL.Snippet
160-
pgFmtIdent x = SQL.sql $ escapeIdent x
160+
pgFmtIdent x = SQL.sql . encodeUtf8 $ escapeIdent x
161161

162-
escapeIdent :: Text -> ByteString
163-
escapeIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
162+
escapeIdent :: Text -> Text
163+
escapeIdent x = "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
164164

165165
-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`
166166
pgFmtLit :: Text -> Text
@@ -181,7 +181,7 @@ trimNullChars = T.takeWhile (/= '\x0')
181181
-- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"]
182182
-- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\""
183183
escapeIdentList :: [Text] -> ByteString
184-
escapeIdentList schemas = BS.intercalate ", " $ escapeIdent <$> schemas
184+
escapeIdentList schemas = BS.intercalate ", " $ encodeUtf8 . escapeIdent <$> schemas
185185

186186
asCsvF :: SQL.Snippet
187187
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF

src/PostgREST/SchemaCache.hs

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import NeatInterpolation (trimming)
4646
import PostgREST.Config (AppConfig (..))
4747
import PostgREST.Config.Database (TimezoneNames,
4848
toIsolationLevel)
49+
import PostgREST.Query.SqlFragment (escapeIdent)
4950
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
5051
QualifiedIdentifier (..),
5152
RelIdentifier (..),
@@ -363,7 +364,7 @@ allFunctions :: Bool -> SQL.Statement AppConfig RoutineMap
363364
allFunctions = SQL.Statement funcsSqlQuery params decodeFuncs
364365
where
365366
params =
366-
(toList . configDbSchemas >$< arrayParam HE.text) <>
367+
(map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>
367368
(configDbHoistedTxSettings >$< arrayParam HE.text)
368369

369370
accessibleFuncs :: Bool -> SQL.Statement ([Schema], [Text]) RoutineMap
@@ -464,7 +465,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
464465
) func_settings ON TRUE
465466
WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true)
466467
AND prokind = 'f'
467-
AND pn.nspname = ANY($$1) |]
468+
AND p.pronamespace = ANY($$1::regnamespace[]) |]
468469

469470
schemaDescription :: Bool -> SQL.Statement Schema (Maybe Text)
470471
schemaDescription =
@@ -477,20 +478,21 @@ schemaDescription =
477478
pg_namespace n
478479
left join pg_description d on d.objoid = n.oid
479480
where
480-
n.nspname = $$1 |]
481+
n.oid = $$1::regnamespace |]
481482

482483
accessibleTables :: Bool -> SQL.Statement [Schema] AccessSet
483484
accessibleTables =
484-
SQL.Statement sql (arrayParam HE.text) decodeAccessibleIdentifiers
485+
SQL.Statement sql params decodeAccessibleIdentifiers
485486
where
487+
params = map escapeIdent >$< arrayParam HE.text
486488
sql = encodeUtf8 [trimming|
487489
SELECT
488490
n.nspname AS table_schema,
489491
c.relname AS table_name
490492
FROM pg_class c
491493
JOIN pg_namespace n ON n.oid = c.relnamespace
492494
WHERE c.relkind IN ('v','r','m','f','p')
493-
AND n.nspname = ANY($$1)
495+
AND c.relnamespace = ANY($$1::regnamespace[])
494496
AND (
495497
pg_has_role(c.relowner, 'USAGE')
496498
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
@@ -608,7 +610,7 @@ addViewPrimaryKeys tabs keyDeps =
608610
allTables :: Bool -> SQL.Statement AppConfig TablesMap
609611
allTables = SQL.Statement tablesSqlQuery params decodeTables
610612
where
611-
params = toList . configDbSchemas >$< arrayParam HE.text
613+
params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
612614

613615
-- | Gets tables with their PK cols
614616
tablesSqlQuery :: SqlQuery
@@ -657,7 +659,7 @@ tablesSqlQuery =
657659
ON d.objoid = a.attrelid and d.objsubid = a.attnum
658660
LEFT JOIN pg_attrdef ad
659661
ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
660-
JOIN (pg_class c JOIN pg_namespace nc ON c.relnamespace = nc.oid)
662+
JOIN pg_class c
661663
ON a.attrelid = c.oid
662664
JOIN pg_type t
663665
ON a.atttypid = t.oid
@@ -670,7 +672,7 @@ tablesSqlQuery =
670672
AND a.attnum > 0
671673
AND NOT a.attisdropped
672674
AND c.relkind in ('r', 'v', 'f', 'm', 'p')
673-
AND nc.nspname = ANY($$1)
675+
AND c.relnamespace = ANY($$1::regnamespace[])
674676
),
675677
columns_agg AS (
676678
SELECT
@@ -856,8 +858,8 @@ allViewsKeyDependencies =
856858
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
857859
where
858860
params =
859-
(toList . configDbSchemas >$< arrayParam HE.text) <>
860-
(configDbExtraSearchPath >$< arrayParam HE.text)
861+
(map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>
862+
(map escapeIdent . toList . configDbExtraSearchPath >$< arrayParam HE.text)
861863
sql = encodeUtf8 [trimming|
862864
with recursive
863865
pks_fks as (
@@ -887,18 +889,19 @@ allViewsKeyDependencies =
887889
),
888890
views as (
889891
select
890-
c.oid as view_id,
891-
n.nspname as view_schema,
892-
c.relname as view_name,
893-
r.ev_action as view_definition
892+
c.oid as view_id,
893+
c.relnamespace as view_schema_id,
894+
n.nspname as view_schema,
895+
c.relname as view_name,
896+
r.ev_action as view_definition
894897
from pg_class c
895898
join pg_namespace n on n.oid = c.relnamespace
896899
join pg_rewrite r on r.ev_class = c.oid
897-
where c.relkind in ('v', 'm') and n.nspname = ANY($$1 || $$2)
900+
where c.relkind in ('v', 'm') and c.relnamespace = ANY($$1::regnamespace[] || $$2::regnamespace[])
898901
),
899902
transform_json as (
900903
select
901-
view_id, view_schema, view_name,
904+
view_id, view_schema_id, view_schema, view_name,
902905
-- the following formatting is without indentation on purpose
903906
-- to allow simple diffs, with less whitespace noise
904907
replace(
@@ -978,30 +981,31 @@ allViewsKeyDependencies =
978981
),
979982
target_entries as(
980983
select
981-
view_id, view_schema, view_name,
984+
view_id, view_schema_id, view_schema, view_name,
982985
json_array_elements(view_definition->0->'targetList') as entry
983986
from transform_json
984987
),
985988
results as(
986989
select
987-
view_id, view_schema, view_name,
990+
view_id, view_schema_id, view_schema, view_name,
988991
(entry->>'resno')::int as view_column,
989992
(entry->>'resorigtbl')::oid as resorigtbl,
990993
(entry->>'resorigcol')::int as resorigcol
991994
from target_entries
992995
),
993996
-- CYCLE detection according to PG docs: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CYCLE
994997
-- Can be replaced with CYCLE clause once PG v13 is EOL.
995-
recursion(view_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as(
998+
recursion(view_id, view_schema_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as(
996999
select
9971000
r.*,
9981001
false,
9991002
ARRAY[resorigtbl]
10001003
from results r
1001-
where view_schema = ANY ($$1)
1004+
where view_schema_id = ANY ($$1::regnamespace[])
10021005
union all
10031006
select
10041007
view.view_id,
1008+
view.view_schema_id,
10051009
view.view_schema,
10061010
view.view_name,
10071011
view.view_column,
@@ -1060,7 +1064,7 @@ mediaHandlers :: Bool -> SQL.Statement AppConfig MediaHandlerMap
10601064
mediaHandlers =
10611065
SQL.Statement sql params decodeMediaHandlers
10621066
where
1063-
params = toList . configDbSchemas >$< arrayParam HE.text
1067+
params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
10641068
sql = encodeUtf8 [trimming|
10651069
with
10661070
all_relations as (
@@ -1101,7 +1105,7 @@ mediaHandlers =
11011105
join pg_type arg_name on arg_name.oid = proc.proargtypes[0]
11021106
join pg_namespace arg_schema on arg_schema.oid = arg_name.typnamespace
11031107
where
1104-
proc_schema.nspname = ANY($$1) and
1108+
proc.pronamespace = ANY($$1::regnamespace[]) and
11051109
proc.pronargs = 1 and
11061110
arg_name.oid in (select reltype from all_relations)
11071111
union
@@ -1117,7 +1121,7 @@ mediaHandlers =
11171121
join media_types mtype on proc.prorettype = mtype.oid
11181122
join pg_namespace typ_sch on typ_sch.oid = mtype.typnamespace
11191123
where
1120-
pro_sch.nspname = ANY($$1) and NOT proretset
1124+
proc.pronamespace = ANY($$1::regnamespace[]) and NOT proretset
11211125
and prokind = 'f'|]
11221126

11231127
decodeMediaHandlers :: HD.Result MediaHandlerMap

test/spec/Feature/Query/ErrorSpec.hs

Lines changed: 0 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,59 +9,6 @@ import Test.Hspec.Wai.JSON
99

1010
import Protolude hiding (get)
1111

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

test/spec/Main.hs

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

127127
extraSearchPathApp = appDbs testCfgExtraSearchPath
128128
unicodeApp = appDbs testUnicodeCfg
129-
nonexistentSchemaApp = appDbs testNonexistentSchemaCfg
130129
multipleSchemaApp = appDbs testMultipleSchemaCfg
131130
ignorePrivOpenApi = appDbs testIgnorePrivOpenApiCfg
132131

@@ -221,10 +220,6 @@ main = do
221220
parallel $ before asymJwkSetApp $
222221
describe "Feature.Auth.AsymmetricJwtSpec" Feature.Auth.AsymmetricJwtSpec.spec
223222

224-
-- this test runs with a nonexistent db-schema
225-
parallel $ before nonexistentSchemaApp $
226-
describe "Feature.Query.NonExistentSchemaErrorSpec" Feature.Query.ErrorSpec.nonExistentSchema
227-
228223
-- this test runs with an extra search path
229224
parallel $ before extraSearchPathApp $ do
230225
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec

test/spec/SpecHelper.hs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,6 @@ testCfgAsymJWKSet =
227227
, configJWKS = rightToMaybe $ parseSecret secret
228228
}
229229

230-
testNonexistentSchemaCfg :: AppConfig
231-
testNonexistentSchemaCfg = baseCfg { configDbSchemas = fromList ["nonexistent"] }
232-
233230
testCfgExtraSearchPath :: AppConfig
234231
testCfgExtraSearchPath = baseCfg { configDbExtraSearchPath = ["public", "extensions", "EXTRA \"@/\\#~_-"] }
235232

0 commit comments

Comments
 (0)