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:
committed by
Wolfgang Walther
parent
f31848f2e5
commit
86c3257f54
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 \"@/\\#~_-"] }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user