From 9c070702636dc42e6ff1113205aa6f73462d0b9b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 Sep 2025 18:45:39 -0500 Subject: [PATCH] refactor: move accessibleTables to SqlFragment --- src/PostgREST/Query.hs | 14 ++++++++-- src/PostgREST/Query/SqlFragment.hs | 20 ++++++++++++++ src/PostgREST/SchemaCache.hs | 33 +----------------------- src/PostgREST/SchemaCache/Identifiers.hs | 4 --- 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/PostgREST/Query.hs b/src/PostgREST/Query.hs index 90c770db2..489380d32 100644 --- a/src/PostgREST/Query.hs +++ b/src/PostgREST/Query.hs @@ -205,8 +205,8 @@ actionQuery (MayUseDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ sCache mainActionQuery = lift $ case configOpenApiMode of OAFollowPriv -> do - tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables configDbPreparedStatements) - schDesc <- SQL.statement mempty (SQL.dynamicallyParameterized (SqlFragment.schemaDescription tSchema) decodeSchemaDesc configDbPreparedStatements) + tableAccess <- SQL.statement mempty $ SQL.dynamicallyParameterized (SqlFragment.accessibleTables tSchema) decodeAccessibleIdentifiers configDbPreparedStatements + schDesc <- SQL.statement mempty $ SQL.dynamicallyParameterized (SqlFragment.schemaDescription tSchema) decodeSchemaDesc configDbPreparedStatements MaybeDbResult plan . Just <$> ((,,) (HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache) @@ -214,6 +214,7 @@ actionQuery (MayUseDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ sCache <*> pure schDesc) OAIgnorePriv -> do schDesc <- SQL.statement mempty (SQL.dynamicallyParameterized (SqlFragment.schemaDescription tSchema) decodeSchemaDesc configDbPreparedStatements) + let tbls = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbTables sCache) routs = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbRoutines sCache) @@ -224,6 +225,15 @@ actionQuery (MayUseDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ sCache decodeSchemaDesc :: HD.Result (Maybe Text) decodeSchemaDesc = join <$> HD.rowMaybe (nullableColumn HD.text) + decodeAccessibleIdentifiers :: HD.Result (S.Set QualifiedIdentifier) + decodeAccessibleIdentifiers = + let + row = QualifiedIdentifier + <$> column HD.text + <*> column HD.text + in + S.fromList <$> HD.rowList row + -- Makes sure the querystring pk matches the payload pk -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, -- PUT /items?id=eq.14 { "id" : 2, .. } is rejected. diff --git a/src/PostgREST/Query/SqlFragment.hs b/src/PostgREST/Query/SqlFragment.hs index b8a6613d2..c961fdc49 100644 --- a/src/PostgREST/Query/SqlFragment.hs +++ b/src/PostgREST/Query/SqlFragment.hs @@ -41,6 +41,7 @@ module PostgREST.Query.SqlFragment , escapeIdent , escapeIdentList , schemaDescription + , accessibleTables ) where import qualified Data.Aeson as JSON @@ -597,3 +598,22 @@ schemaDescription schema = "SELECT pg_catalog.obj_description(" <> encoded <> "::regnamespace, 'pg_namespace')" where encoded = SQL.encoderAndParam (HE.nonNullable HE.unknown) $ encodeUtf8 schema + +accessibleTables :: Text -> SQL.Snippet +accessibleTables schema = SQL.sql (encodeUtf8 [trimming| + SELECT + n.nspname AS table_schema, + c.relname AS table_name + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','r','m','f','p') + AND c.relnamespace = |]) <> encodedSchema <> "::regnamespace " <> SQL.sql (encodeUtf8 [trimming| + AND ( + pg_has_role(c.relowner, 'USAGE') + or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') + or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') + ) + AND not c.relispartition + ORDER BY table_schema, table_name|]) + where + encodedSchema = SQL.encoderAndParam (HE.nonNullable HE.text) schema diff --git a/src/PostgREST/SchemaCache.hs b/src/PostgREST/SchemaCache.hs index 59d4bb480..30820ec75 100644 --- a/src/PostgREST/SchemaCache.hs +++ b/src/PostgREST/SchemaCache.hs @@ -21,7 +21,6 @@ These queries are executed once at startup or when PostgREST is reloaded. module PostgREST.SchemaCache ( SchemaCache(..) , querySchemaCache - , accessibleTables , accessibleFuncs , showSummary ) where @@ -46,7 +45,7 @@ import PostgREST.Config (AppConfig (..)) import PostgREST.Config.Database (TimezoneNames, toIsolationLevel) import PostgREST.Query.SqlFragment (escapeIdent) -import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName, +import PostgREST.SchemaCache.Identifiers (FieldName, QualifiedIdentifier (..), RelIdentifier (..), Schema, isAnyElement) @@ -208,14 +207,6 @@ removeInternal schemas dbStruct = M2M Junction{junTable} -> qiSchema junTable `notElem` schemas _ -> False -decodeAccessibleIdentifiers :: HD.Result AccessSet -decodeAccessibleIdentifiers = - S.fromList <$> HD.rowList row - where - row = QualifiedIdentifier - <$> column HD.text - <*> column HD.text - decodeTables :: HD.Result TablesMap decodeTables = HM.fromList . map (\tbl@Table{tableSchema, tableName} -> (QualifiedIdentifier tableSchema tableName, tbl)) <$> HD.rowList tblRow @@ -473,28 +464,6 @@ funcsSqlQuery = encodeUtf8 [trimming| WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true) AND prokind = 'f' AND p.pronamespace = ANY($$1::regnamespace[]) |] - -accessibleTables :: Bool -> SQL.Statement [Schema] AccessSet -accessibleTables = - SQL.Statement sql params decodeAccessibleIdentifiers - where - params = map escapeIdent >$< arrayParam HE.text - sql = encodeUtf8 [trimming| - SELECT - n.nspname AS table_schema, - c.relname AS table_name - FROM pg_class c - JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('v','r','m','f','p') - AND c.relnamespace = ANY($$1::regnamespace[]) - AND ( - pg_has_role(c.relowner, 'USAGE') - or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER') - or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES') - ) - AND not c.relispartition - ORDER BY table_schema, table_name|] - {- 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. diff --git a/src/PostgREST/SchemaCache/Identifiers.hs b/src/PostgREST/SchemaCache/Identifiers.hs index 6d2a4cffa..2eaf42b3f 100644 --- a/src/PostgREST/SchemaCache/Identifiers.hs +++ b/src/PostgREST/SchemaCache/Identifiers.hs @@ -8,13 +8,11 @@ module PostgREST.SchemaCache.Identifiers , Schema , TableName , FieldName - , AccessSet , dumpQi , toQi ) where import qualified Data.Aeson as JSON -import qualified Data.Set as S import qualified Data.Text as T import Protolude @@ -51,5 +49,3 @@ toQi txt = case T.drop 1 <$> T.breakOn "." txt of type Schema = Text type TableName = Text type FieldName = Text - -type AccessSet = S.Set QualifiedIdentifier