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.
This commit is contained in:
Wolfgang Walther
2024-07-09 08:31:31 +02:00
committed by Wolfgang Walther
parent f31848f2e5
commit 86c3257f54
6 changed files with 33 additions and 88 deletions
+2
View File
@@ -25,6 +25,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther - #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther
- #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7 - #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7
- #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla - #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla
- #3644, Fail schema cache lookup with invalid db-schemas config - @wolfgangwalther
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
## [12.2.1] - 2024-06-27 ## [12.2.1] - 2024-06-27
+4 -4
View File
@@ -157,10 +157,10 @@ pgBuildArrayLiteral vals =
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833 -- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
pgFmtIdent :: Text -> SQL.Snippet pgFmtIdent :: Text -> SQL.Snippet
pgFmtIdent x = SQL.sql $ escapeIdent x pgFmtIdent x = SQL.sql . encodeUtf8 $ escapeIdent x
escapeIdent :: Text -> ByteString escapeIdent :: Text -> Text
escapeIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\"" escapeIdent x = "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..` -- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`
pgFmtLit :: Text -> Text pgFmtLit :: Text -> Text
@@ -181,7 +181,7 @@ trimNullChars = T.takeWhile (/= '\x0')
-- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"] -- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"]
-- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\"" -- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\""
escapeIdentList :: [Text] -> ByteString escapeIdentList :: [Text] -> ByteString
escapeIdentList schemas = BS.intercalate ", " $ escapeIdent <$> schemas escapeIdentList schemas = BS.intercalate ", " $ encodeUtf8 . escapeIdent <$> schemas
asCsvF :: SQL.Snippet asCsvF :: SQL.Snippet
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
+27 -23
View File
@@ -46,6 +46,7 @@ import NeatInterpolation (trimming)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Config.Database (TimezoneNames, import PostgREST.Config.Database (TimezoneNames,
toIsolationLevel) toIsolationLevel)
import PostgREST.Query.SqlFragment (escapeIdent)
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName, import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
QualifiedIdentifier (..), QualifiedIdentifier (..),
RelIdentifier (..), RelIdentifier (..),
@@ -363,7 +364,7 @@ allFunctions :: Bool -> SQL.Statement AppConfig RoutineMap
allFunctions = SQL.Statement funcsSqlQuery params decodeFuncs allFunctions = SQL.Statement funcsSqlQuery params decodeFuncs
where where
params = params =
(toList . configDbSchemas >$< arrayParam HE.text) <> (map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>
(configDbHoistedTxSettings >$< arrayParam HE.text) (configDbHoistedTxSettings >$< arrayParam HE.text)
accessibleFuncs :: Bool -> SQL.Statement ([Schema], [Text]) RoutineMap accessibleFuncs :: Bool -> SQL.Statement ([Schema], [Text]) RoutineMap
@@ -464,7 +465,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
) func_settings ON TRUE ) func_settings ON TRUE
WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true) WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true)
AND prokind = 'f' AND prokind = 'f'
AND pn.nspname = ANY($$1) |] AND p.pronamespace = ANY($$1::regnamespace[]) |]
schemaDescription :: Bool -> SQL.Statement Schema (Maybe Text) schemaDescription :: Bool -> SQL.Statement Schema (Maybe Text)
schemaDescription = schemaDescription =
@@ -477,12 +478,13 @@ schemaDescription =
pg_namespace n pg_namespace n
left join pg_description d on d.objoid = n.oid left join pg_description d on d.objoid = n.oid
where where
n.nspname = $$1 |] n.oid = $$1::regnamespace |]
accessibleTables :: Bool -> SQL.Statement [Schema] AccessSet accessibleTables :: Bool -> SQL.Statement [Schema] AccessSet
accessibleTables = accessibleTables =
SQL.Statement sql (arrayParam HE.text) decodeAccessibleIdentifiers SQL.Statement sql params decodeAccessibleIdentifiers
where where
params = map escapeIdent >$< arrayParam HE.text
sql = encodeUtf8 [trimming| sql = encodeUtf8 [trimming|
SELECT SELECT
n.nspname AS table_schema, n.nspname AS table_schema,
@@ -490,7 +492,7 @@ accessibleTables =
FROM pg_class c FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('v','r','m','f','p') WHERE c.relkind IN ('v','r','m','f','p')
AND n.nspname = ANY($$1) AND c.relnamespace = ANY($$1::regnamespace[])
AND ( AND (
pg_has_role(c.relowner, 'USAGE') pg_has_role(c.relowner, 'USAGE')
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
@@ -608,7 +610,7 @@ addViewPrimaryKeys tabs keyDeps =
allTables :: Bool -> SQL.Statement AppConfig TablesMap allTables :: Bool -> SQL.Statement AppConfig TablesMap
allTables = SQL.Statement tablesSqlQuery params decodeTables allTables = SQL.Statement tablesSqlQuery params decodeTables
where where
params = toList . configDbSchemas >$< arrayParam HE.text params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
-- | Gets tables with their PK cols -- | Gets tables with their PK cols
tablesSqlQuery :: SqlQuery tablesSqlQuery :: SqlQuery
@@ -657,7 +659,7 @@ tablesSqlQuery =
ON d.objoid = a.attrelid and d.objsubid = a.attnum ON d.objoid = a.attrelid and d.objsubid = a.attnum
LEFT JOIN pg_attrdef ad LEFT JOIN pg_attrdef ad
ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
JOIN (pg_class c JOIN pg_namespace nc ON c.relnamespace = nc.oid) JOIN pg_class c
ON a.attrelid = c.oid ON a.attrelid = c.oid
JOIN pg_type t JOIN pg_type t
ON a.atttypid = t.oid ON a.atttypid = t.oid
@@ -670,7 +672,7 @@ tablesSqlQuery =
AND a.attnum > 0 AND a.attnum > 0
AND NOT a.attisdropped AND NOT a.attisdropped
AND c.relkind in ('r', 'v', 'f', 'm', 'p') AND c.relkind in ('r', 'v', 'f', 'm', 'p')
AND nc.nspname = ANY($$1) AND c.relnamespace = ANY($$1::regnamespace[])
), ),
columns_agg AS ( columns_agg AS (
SELECT SELECT
@@ -856,8 +858,8 @@ allViewsKeyDependencies =
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089 -- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
where where
params = params =
(toList . configDbSchemas >$< arrayParam HE.text) <> (map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text) <>
(configDbExtraSearchPath >$< arrayParam HE.text) (map escapeIdent . toList . configDbExtraSearchPath >$< arrayParam HE.text)
sql = encodeUtf8 [trimming| sql = encodeUtf8 [trimming|
with recursive with recursive
pks_fks as ( pks_fks as (
@@ -887,18 +889,19 @@ allViewsKeyDependencies =
), ),
views as ( views as (
select select
c.oid as view_id, c.oid as view_id,
n.nspname as view_schema, c.relnamespace as view_schema_id,
c.relname as view_name, n.nspname as view_schema,
r.ev_action as view_definition c.relname as view_name,
r.ev_action as view_definition
from pg_class c from pg_class c
join pg_namespace n on n.oid = c.relnamespace join pg_namespace n on n.oid = c.relnamespace
join pg_rewrite r on r.ev_class = c.oid join pg_rewrite r on r.ev_class = c.oid
where c.relkind in ('v', 'm') and n.nspname = ANY($$1 || $$2) where c.relkind in ('v', 'm') and c.relnamespace = ANY($$1::regnamespace[] || $$2::regnamespace[])
), ),
transform_json as ( transform_json as (
select select
view_id, view_schema, view_name, view_id, view_schema_id, view_schema, view_name,
-- the following formatting is without indentation on purpose -- the following formatting is without indentation on purpose
-- to allow simple diffs, with less whitespace noise -- to allow simple diffs, with less whitespace noise
replace( replace(
@@ -978,13 +981,13 @@ allViewsKeyDependencies =
), ),
target_entries as( target_entries as(
select select
view_id, view_schema, view_name, view_id, view_schema_id, view_schema, view_name,
json_array_elements(view_definition->0->'targetList') as entry json_array_elements(view_definition->0->'targetList') as entry
from transform_json from transform_json
), ),
results as( results as(
select select
view_id, view_schema, view_name, view_id, view_schema_id, view_schema, view_name,
(entry->>'resno')::int as view_column, (entry->>'resno')::int as view_column,
(entry->>'resorigtbl')::oid as resorigtbl, (entry->>'resorigtbl')::oid as resorigtbl,
(entry->>'resorigcol')::int as resorigcol (entry->>'resorigcol')::int as resorigcol
@@ -992,16 +995,17 @@ allViewsKeyDependencies =
), ),
-- CYCLE detection according to PG docs: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CYCLE -- CYCLE detection according to PG docs: https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-CYCLE
-- Can be replaced with CYCLE clause once PG v13 is EOL. -- Can be replaced with CYCLE clause once PG v13 is EOL.
recursion(view_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as( recursion(view_id, view_schema_id, view_schema, view_name, view_column, resorigtbl, resorigcol, is_cycle, path) as(
select select
r.*, r.*,
false, false,
ARRAY[resorigtbl] ARRAY[resorigtbl]
from results r from results r
where view_schema = ANY ($$1) where view_schema_id = ANY ($$1::regnamespace[])
union all union all
select select
view.view_id, view.view_id,
view.view_schema_id,
view.view_schema, view.view_schema,
view.view_name, view.view_name,
view.view_column, view.view_column,
@@ -1060,7 +1064,7 @@ mediaHandlers :: Bool -> SQL.Statement AppConfig MediaHandlerMap
mediaHandlers = mediaHandlers =
SQL.Statement sql params decodeMediaHandlers SQL.Statement sql params decodeMediaHandlers
where where
params = toList . configDbSchemas >$< arrayParam HE.text params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
sql = encodeUtf8 [trimming| sql = encodeUtf8 [trimming|
with with
all_relations as ( all_relations as (
@@ -1101,7 +1105,7 @@ mediaHandlers =
join pg_type arg_name on arg_name.oid = proc.proargtypes[0] join pg_type arg_name on arg_name.oid = proc.proargtypes[0]
join pg_namespace arg_schema on arg_schema.oid = arg_name.typnamespace join pg_namespace arg_schema on arg_schema.oid = arg_name.typnamespace
where where
proc_schema.nspname = ANY($$1) and proc.pronamespace = ANY($$1::regnamespace[]) and
proc.pronargs = 1 and proc.pronargs = 1 and
arg_name.oid in (select reltype from all_relations) arg_name.oid in (select reltype from all_relations)
union union
@@ -1117,7 +1121,7 @@ mediaHandlers =
join media_types mtype on proc.prorettype = mtype.oid join media_types mtype on proc.prorettype = mtype.oid
join pg_namespace typ_sch on typ_sch.oid = mtype.typnamespace join pg_namespace typ_sch on typ_sch.oid = mtype.typnamespace
where where
pro_sch.nspname = ANY($$1) and NOT proretset proc.pronamespace = ANY($$1::regnamespace[]) and NOT proretset
and prokind = 'f'|] and prokind = 'f'|]
decodeMediaHandlers :: HD.Result MediaHandlerMap decodeMediaHandlers :: HD.Result MediaHandlerMap
-53
View File
@@ -9,59 +9,6 @@ import Test.Hspec.Wai.JSON
import Protolude hiding (get) import Protolude hiding (get)
nonExistentSchema :: SpecWith ((), Application)
nonExistentSchema = do
describe "Non existent api schema" $ do
it "succeeds when requesting root path" $
get "/" `shouldRespondWith` 200
it "gives 404 when requesting a nonexistent table in this nonexistent schema" $
get "/nonexistent_table" `shouldRespondWith` 404
describe "Non existent URL" $ do
it "gives 404 on a single nested route" $
get "/projects/nested" `shouldRespondWith` 404
it "gives 404 on a double nested route" $
get "/projects/nested/double" `shouldRespondWith` 404
describe "Unsupported HTTP methods" $ do
it "should return 405 for CONNECT method" $
request methodConnect "/"
[]
""
`shouldRespondWith`
[json|
{"hint": null,
"details": null,
"code": "PGRST117",
"message":"Unsupported HTTP method: CONNECT"}|]
{ matchStatus = 405 }
it "should return 405 for TRACE method" $
request methodTrace "/"
[]
""
`shouldRespondWith`
[json|
{"hint": null,
"details": null,
"code": "PGRST117",
"message":"Unsupported HTTP method: TRACE"}|]
{ matchStatus = 405 }
it "should return 405 for OTHER method" $
request "OTHER" "/"
[]
""
`shouldRespondWith`
[json|
{"hint": null,
"details": null,
"code": "PGRST117",
"message":"Unsupported HTTP method: OTHER"}|]
{ matchStatus = 405 }
pgErrorCodeMapping :: SpecWith ((), Application) pgErrorCodeMapping :: SpecWith ((), Application)
pgErrorCodeMapping = do pgErrorCodeMapping = do
describe "PostreSQL error code mappings" $ do describe "PostreSQL error code mappings" $ do
-5
View File
@@ -126,7 +126,6 @@ main = do
extraSearchPathApp = appDbs testCfgExtraSearchPath extraSearchPathApp = appDbs testCfgExtraSearchPath
unicodeApp = appDbs testUnicodeCfg unicodeApp = appDbs testUnicodeCfg
nonexistentSchemaApp = appDbs testNonexistentSchemaCfg
multipleSchemaApp = appDbs testMultipleSchemaCfg multipleSchemaApp = appDbs testMultipleSchemaCfg
ignorePrivOpenApi = appDbs testIgnorePrivOpenApiCfg ignorePrivOpenApi = appDbs testIgnorePrivOpenApiCfg
@@ -221,10 +220,6 @@ main = do
parallel $ before asymJwkSetApp $ parallel $ before asymJwkSetApp $
describe "Feature.Auth.AsymmetricJwtSpec" Feature.Auth.AsymmetricJwtSpec.spec describe "Feature.Auth.AsymmetricJwtSpec" Feature.Auth.AsymmetricJwtSpec.spec
-- this test runs with a nonexistent db-schema
parallel $ before nonexistentSchemaApp $
describe "Feature.Query.NonExistentSchemaErrorSpec" Feature.Query.ErrorSpec.nonExistentSchema
-- this test runs with an extra search path -- this test runs with an extra search path
parallel $ before extraSearchPathApp $ do parallel $ before extraSearchPathApp $ do
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
-3
View File
@@ -227,9 +227,6 @@ testCfgAsymJWKSet =
, configJWKS = rightToMaybe $ parseSecret secret , configJWKS = rightToMaybe $ parseSecret secret
} }
testNonexistentSchemaCfg :: AppConfig
testNonexistentSchemaCfg = baseCfg { configDbSchemas = fromList ["nonexistent"] }
testCfgExtraSearchPath :: AppConfig testCfgExtraSearchPath :: AppConfig
testCfgExtraSearchPath = baseCfg { configDbExtraSearchPath = ["public", "extensions", "EXTRA \"@/\\#~_-"] } testCfgExtraSearchPath = baseCfg { configDbExtraSearchPath = ["public", "extensions", "EXTRA \"@/\\#~_-"] }