restrict openapi spec based on sql grants

This commit is contained in:
2026-08-15 21:41:45 +02:00
parent a8feaadc01
commit 4a5d626112
11 changed files with 209 additions and 54 deletions
+4
View File
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. From versio
## Unreleased ## Unreleased
### Fixed
- The OpenAPI output now reflects table privileges: only the granted HTTP methods are exposed (e.g. `SELECT` grants `GET`, `INSERT` grants `POST`) and column-level grants filter the columns shown on table definitions and row filters.
## [16.1] - 2026-08-10 ## [16.1] - 2026-08-10
### Fixed ### Fixed
+2
View File
@@ -9,6 +9,8 @@ PostgREST automatically serves a full `OpenAPI <https://www.openapis.org/>`_ des
By default, this output depends on the permissions of the role that is contained in the JWT role claim (or the :ref:`db-anon-role` if no JWT is sent). If you need to show all the endpoints disregarding the role's permissions, set the :ref:`openapi-mode` config to :code:`ignore-privileges`. By default, this output depends on the permissions of the role that is contained in the JWT role claim (or the :ref:`db-anon-role` if no JWT is sent). If you need to show all the endpoints disregarding the role's permissions, set the :ref:`openapi-mode` config to :code:`ignore-privileges`.
When following privileges, the output reflects both the granted HTTP methods and columns: a relation with only ``SELECT`` will only expose ``GET``, a relation with only ``INSERT`` will only expose ``POST``, and column-level grants limit the columns shown on the table definitions and row filters.
For extra customization, the OpenAPI output contains a "description" field for every `SQL comment <https://www.postgresql.org/docs/current/sql-comment.html>`_ on any database object. For instance, For extra customization, the OpenAPI output contains a "description" field for every `SQL comment <https://www.postgresql.org/docs/current/sql-comment.html>`_ on any database object. For instance,
.. code-block:: postgres .. code-block:: postgres
+1
View File
@@ -78,6 +78,7 @@ library
PostgREST.Network PostgREST.Network
PostgREST.Observation PostgREST.Observation
PostgREST.Query PostgREST.Query
PostgREST.Query.OpenApi
PostgREST.Query.PreQuery PostgREST.Query.PreQuery
PostgREST.Query.QueryBuilder PostgREST.Query.QueryBuilder
PostgREST.Query.SqlFragment PostgREST.Query.SqlFragment
+15 -12
View File
@@ -19,7 +19,6 @@ import qualified Data.Aeson.Lens as L
import qualified Data.ByteString as BS hiding (break) import qualified Data.ByteString as BS hiding (break)
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Set as S
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.DynamicStatements.Statement as SQL import qualified Hasql.DynamicStatements.Statement as SQL
import qualified Hasql.Session as SQL (Session) import qualified Hasql.Session as SQL (Session)
@@ -44,6 +43,7 @@ import PostgREST.Plan (ActionPlan (..), CrudPlan (..),
DbActionPlan (..), InfoPlan (..), DbActionPlan (..), InfoPlan (..),
InspectPlan (..)) InspectPlan (..))
import PostgREST.Query (MainQuery (..)) import PostgREST.Query (MainQuery (..))
import PostgREST.Query.OpenApi (TableAccess (..), TablesAccess)
import PostgREST.SchemaCache (SchemaCache (..)) import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap) import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap)
@@ -60,7 +60,7 @@ data MainTx
data DbResult data DbResult
= DbCrudResult CrudPlan ResultSet = DbCrudResult CrudPlan ResultSet
| DbPlanResult MediaType BS.ByteString | DbPlanResult MediaType BS.ByteString
| MaybeDbResult InspectPlan (Maybe (TablesMap, RoutineMap, Maybe Text)) | MaybeDbResult InspectPlan (Maybe (TablesMap, TablesAccess, RoutineMap, Maybe Text))
| NoDbResult InfoPlan | NoDbResult InfoPlan
-- | Standard result set format used for the mqMain query -- | Standard result set format used for the mqMain query
@@ -174,33 +174,36 @@ actionResult MainQuery{mqOpenAPI=(tblsQ, funcsQ, schQ)} (MayUseDb plan@InspectPl
mainActionQuery = lift $ mainActionQuery = lift $
case configOpenApiMode of case configOpenApiMode of
OAFollowPriv -> do OAFollowPriv -> do
tableAccess <- SQL.statement mempty $ SQL.dynamicallyParameterized tblsQ decodeAccessibleIdentifiers configDbPreparedStatements tableAccess <- SQL.statement mempty $ SQL.dynamicallyParameterized tblsQ decodeTablesAccess configDbPreparedStatements
accFuncs <- SQL.statement mempty $ SQL.dynamicallyParameterized funcsQ SchemaCache.decodeFuncs configDbPreparedStatements accFuncs <- SQL.statement mempty $ SQL.dynamicallyParameterized funcsQ SchemaCache.decodeFuncs configDbPreparedStatements
schDesc <- SQL.statement mempty $ SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements schDesc <- SQL.statement mempty $ SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements
let tbls = HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache let tbls = HM.filterWithKey (\qi _ -> HM.member qi tableAccess) $ SchemaCache.dbTables sCache
pure $ MaybeDbResult plan (Just (tbls, accFuncs, schDesc)) pure $ MaybeDbResult plan (Just (tbls, tableAccess, accFuncs, schDesc))
OAIgnorePriv -> do OAIgnorePriv -> do
schDesc <- SQL.statement mempty (SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements) schDesc <- SQL.statement mempty (SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements)
let tbls = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbTables sCache) let tbls = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbTables sCache)
routs = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbRoutines sCache) routs = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbRoutines sCache)
pure $ MaybeDbResult plan (Just (tbls, routs, schDesc)) pure $ MaybeDbResult plan (Just (tbls, mempty, routs, schDesc))
OADisabled -> OADisabled ->
pure $ MaybeDbResult plan Nothing pure $ MaybeDbResult plan Nothing
decodeSchemaDesc :: HD.Result (Maybe Text) decodeSchemaDesc :: HD.Result (Maybe Text)
decodeSchemaDesc = join <$> HD.rowMaybe (nullableColumn HD.text) decodeSchemaDesc = join <$> HD.rowMaybe (nullableColumn HD.text)
decodeAccessibleIdentifiers :: HD.Result (S.Set QualifiedIdentifier) decodeTablesAccess :: HD.Result TablesAccess
decodeAccessibleIdentifiers = decodeTablesAccess =
let let
row = QualifiedIdentifier row = (,) <$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
<$> column HD.text <*> (TableAccess
<*> column HD.text <$> arrayColumn HD.text
<*> arrayColumn HD.text
<*> arrayColumn HD.text
<*> column HD.bool)
in in
S.fromList <$> HD.rowList row HM.fromList <$> HD.rowList row
-- Makes sure the querystring pk matches the payload pk -- Makes sure the querystring pk matches the payload pk
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
+29
View File
@@ -0,0 +1,29 @@
{-|
Module : PostgREST.Query.OpenApi
Description : Types for reflecting the role privileges on the OpenAPI output.
-}
module PostgREST.Query.OpenApi
( TableAccess (..)
, TablesAccess
) where
import qualified Data.HashMap.Strict as HM
import PostgREST.SchemaCache.Identifiers (FieldName, QualifiedIdentifier)
import Protolude
-- | Privileges that a role has on a relation, used to reflect them on the OpenAPI output.
data TableAccess = TableAccess
{ taSelectCols :: [FieldName]
-- ^ columns the role can SELECT
, taInsertCols :: [FieldName]
-- ^ columns the role can INSERT into
, taUpdateCols :: [FieldName]
-- ^ columns the role can UPDATE
, taDelete :: Bool
-- ^ whether the role can DELETE rows
}
deriving (Show, Eq)
type TablesAccess = HM.HashMap QualifiedIdentifier TableAccess
+26 -1
View File
@@ -598,7 +598,32 @@ accessibleTables :: Text -> SQL.Snippet
accessibleTables schema = SQL.sql (encodeUtf8 [trimming| accessibleTables schema = SQL.sql (encodeUtf8 [trimming|
SELECT SELECT
n.nspname AS table_schema, n.nspname AS table_schema,
c.relname AS table_name c.relname AS table_name,
COALESCE((
SELECT array_agg(a.attname ORDER BY a.attnum)
FROM pg_attribute a
WHERE a.attrelid = c.oid
AND a.attnum > 0
AND NOT a.attisdropped
AND has_column_privilege(c.oid, a.attnum, 'SELECT')
), '{}') AS select_cols,
COALESCE((
SELECT array_agg(a.attname ORDER BY a.attnum)
FROM pg_attribute a
WHERE a.attrelid = c.oid
AND a.attnum > 0
AND NOT a.attisdropped
AND has_column_privilege(c.oid, a.attnum, 'INSERT')
), '{}') AS insert_cols,
COALESCE((
SELECT array_agg(a.attname ORDER BY a.attnum)
FROM pg_attribute a
WHERE a.attrelid = c.oid
AND a.attnum > 0
AND NOT a.attisdropped
AND has_column_privilege(c.oid, a.attnum, 'UPDATE')
), '{}') AS update_cols,
has_table_privilege(c.oid, 'DELETE') AS has_delete
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')
+1 -1
View File
@@ -202,7 +202,7 @@ actionResponse (DbPlanResult media plan) ctxApiRequest _ _ _ =
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) ApiRequest{..} versions conf sCache = actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) ApiRequest{..} versions conf sCache =
let let
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body rsBody = maybe mempty (\(tbls, tblAccess, procs, schDesc) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache tbls tblAccess procs schDesc) body
cLHeader = if headersOnly then mempty else [contentLengthHeader rsBody] cLHeader = if headersOnly then mempty else [contentLengthHeader rsBody]
in in
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader iSchema iNegotiatedByProfile)) rsBody Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader iSchema iNegotiatedByProfile)) rsBody
+71 -40
View File
@@ -27,8 +27,9 @@ import PostgREST.Config (AppConfig (..), Proxy (..),
isMalformedProxyUri, toURI) isMalformedProxyUri, toURI)
import PostgREST.MediaType import PostgREST.MediaType
import PostgREST.Network (escapeHostName) import PostgREST.Network (escapeHostName)
import PostgREST.Query.OpenApi (TableAccess (..), TablesAccess)
import PostgREST.SchemaCache (SchemaCache (..)) import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (FieldName, QualifiedIdentifier (..))
import PostgREST.SchemaCache.Relationship (Cardinality (..), Relationship (..), import PostgREST.SchemaCache.Relationship (Cardinality (..), Relationship (..),
RelationshipsMap) RelationshipsMap)
import PostgREST.SchemaCache.Routine (FuncVolatility (..), Routine (..), import PostgREST.SchemaCache.Routine (FuncVolatility (..), Routine (..),
@@ -38,18 +39,27 @@ import PostgREST.SchemaCache.Table (Column (..), Table (..), TablesMap,
import Protolude hiding (Proxy, get) import Protolude hiding (Proxy, get)
encode :: (Text, Text) -> AppConfig -> SchemaCache -> TablesMap -> HM.HashMap k [Routine] -> Maybe Text -> LBS.ByteString encode :: (Text, Text) -> AppConfig -> SchemaCache -> TablesMap -> TablesAccess -> HM.HashMap k [Routine] -> Maybe Text -> LBS.ByteString
encode versions conf sCache tables procs schemaDescription = encode versions conf sCache tables access procs schemaDescription =
JSON.encode $ JSON.encode $
postgrestSpec postgrestSpec
versions versions
(dbRelationships sCache) (dbRelationships sCache)
(concat $ HM.elems procs) (concat $ HM.elems procs)
(snd <$> HM.toList tables) (fmap (\(_, t) -> (t, accessFor access t)) (HM.toList tables))
(proxyUri conf) (proxyUri conf)
schemaDescription schemaDescription
(configOpenApiSecurityActive conf) (configOpenApiSecurityActive conf)
-- | Get the access privileges for a table. When the table is not present in the
-- map(ignore-privileges mode), assume the role has full access to it.
accessFor :: TablesAccess -> Table -> TableAccess
accessFor access t =
fromMaybe fullAccess (HM.lookup (QualifiedIdentifier (tableSchema t) (tableName t)) access)
where
fullAccess = TableAccess allCols allCols allCols True
allCols = colName <$> tableColumnsList t
makeMimeList :: [MediaType] -> MimeList makeMimeList :: [MediaType] -> MimeList
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
@@ -97,14 +107,19 @@ parseDefault colType colDefault =
where where
wrapInQuotations text = "\"" <> text <> "\"" wrapInQuotations text = "\"" <> text <> "\""
makeTableDef :: RelationshipsMap -> Table -> (Text, Schema) makeTableDef :: RelationshipsMap -> (Table, TableAccess) -> (Text, Schema)
makeTableDef rels t = makeTableDef rels (t, access) =
let tn = tableName t in (tn, (mempty :: Schema)
(tn, (mempty :: Schema) & description .~ tableDescription t
& description .~ tableDescription t & type_ ?~ SwaggerObject
& type_ ?~ SwaggerObject & properties .~ fromList (makeProperty t rels <$> cols)
& properties .~ fromList (makeProperty t rels <$> tableColumnsList t) & required .~ fmap colName (filter (not . colNullable) cols))
& required .~ fmap colName (filter (not . colNullable) $ tableColumnsList t)) where
tn = tableName t
cols = accessibleCols t (taSelectCols access)
accessibleCols :: Table -> [FieldName] -> [Column]
accessibleCols t cols = filter ((`elem` cols) . colName) (tableColumnsList t)
makeProperty :: Table -> RelationshipsMap -> Column -> (Text, Referenced Schema) makeProperty :: Table -> RelationshipsMap -> Column -> (Text, Referenced Schema)
makeProperty tbl rels col = (colName col, Inline s) makeProperty tbl rels col = (colName col, Inline s)
@@ -222,8 +237,8 @@ makeProcPostParams pd =
, Ref $ Reference "preferParams" , Ref $ Reference "preferParams"
] ]
makeParamDefs :: [Table] -> [(Text, Param)] makeParamDefs :: RelationshipsMap -> [(Table, TableAccess)] -> [(Text, Param)]
makeParamDefs ti = makeParamDefs rels tis =
-- TODO: create Prefer for each method (GET, PATCH, etc.) -- TODO: create Prefer for each method (GET, PATCH, etc.)
[ ("preferParams", makePreferParam ["params"]) [ ("preferParams", makePreferParam ["params"])
, ("preferReturn", makePreferParam ["return"]) , ("preferReturn", makePreferParam ["return"])
@@ -280,17 +295,27 @@ makeParamDefs ti =
& in_ .~ ParamQuery & in_ .~ ParamQuery
& type_ ?~ SwaggerString)) & type_ ?~ SwaggerString))
] ]
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) (tableColumnsList t) <> concat [ makeObjectBody rels t access <> makeRowFilters (tableName t) (accessibleCols t (taSelectCols access))
| t <- ti | (t, access) <- tis
] ]
makeObjectBody :: Text -> (Text, Param) makeObjectBody :: RelationshipsMap -> Table -> TableAccess -> [(Text, Param)]
makeObjectBody tn = makeObjectBody rels t access =
("body." <> tn, (mempty :: Param) [ ("body." <> tn, makeBodyParam (taInsertCols access))
& name .~ tn , ("body." <> tn <> ".patch", makeBodyParam (taUpdateCols access))
& description ?~ tn ]
& required ?~ False where
& schema .~ ParamBody (Ref (Reference tn))) tn = tableName t
makeBodyParam cols = (mempty :: Param)
& name .~ tn
& description ?~ tn
& required ?~ False
& schema .~ ParamBody (Inline bodySchema)
where
bodySchema = (mempty :: Schema)
& type_ ?~ SwaggerObject
& properties .~ fromList (makeProperty t rels <$> accessibleCols t cols)
& required .~ fmap colName (filter (not . colNullable) (accessibleCols t cols))
makeRowFilter :: Text -> Column -> (Text, Param) makeRowFilter :: Text -> Column -> (Text, Param)
makeRowFilter tn c = makeRowFilter tn c =
@@ -305,8 +330,8 @@ makeRowFilter tn c =
makeRowFilters :: Text -> [Column] -> [(Text, Param)] makeRowFilters :: Text -> [Column] -> [(Text, Param)]
makeRowFilters tn = fmap (makeRowFilter tn) makeRowFilters tn = fmap (makeRowFilter tn)
makePathItem :: Table -> (FilePath, PathItem) makePathItem :: (Table, TableAccess) -> (FilePath, PathItem)
makePathItem t = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t) makePathItem (t, access) = ("/" ++ T.unpack tn, p)
where where
-- Use first line of table description as summary; rest as description (if present) -- Use first line of table description as summary; rest as description (if present)
-- We strip leading newlines from description so that users can include a blank line between summary and description -- We strip leading newlines from description so that users can include a blank line between summary and description
@@ -327,20 +352,26 @@ makePathItem t = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t
) )
) )
postOp = tOp postOp = tOp
& parameters .~ fmap ref ["body." <> tn, "select", "preferPost"] & parameters .~ fmap ref [bodyParam, "select", "preferPost"]
& at 201 ?~ "Created" & at 201 ?~ "Created"
patchOp = tOp patchOp = tOp
& parameters .~ fmap ref (rs <> ["body." <> tn, "preferReturn"]) & parameters .~ fmap ref (rs <> [patchBodyParam, "preferReturn"])
& at 204 ?~ "No Content" & at 204 ?~ "No Content"
deletOp = tOp deletOp = tOp
& parameters .~ fmap ref (rs <> ["preferReturn"]) & parameters .~ fmap ref (rs <> ["preferReturn"])
& at 204 ?~ "No Content" & at 204 ?~ "No Content"
pr = (mempty :: PathItem) & get ?~ getOp p = (mempty :: PathItem)
pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp & get .~ (if not (null selCols) then Just getOp else Nothing)
p False = pr & post .~ (if tableInsertable t && not (null insCols) then Just postOp else Nothing)
p True = pw & patch .~ (if tableUpdatable t && not (null updCols) then Just patchOp else Nothing)
& delete .~ (if tableDeletable t && taDelete access then Just deletOp else Nothing)
tn = tableName t tn = tableName t
rs = [ T.intercalate "." ["rowFilter", tn, colName c ] | c <- tableColumnsList t ] selCols = accessibleCols t (taSelectCols access)
insCols = accessibleCols t (taInsertCols access)
updCols = accessibleCols t (taUpdateCols access)
rs = [ T.intercalate "." ["rowFilter", tn, colName c ] | c <- selCols ]
bodyParam = "body." <> tn
patchBodyParam = "body." <> tn <> ".patch"
ref = Ref . Reference ref = Ref . Reference
makeProcPathItem :: Routine -> (FilePath, PathItem) makeProcPathItem :: Routine -> (FilePath, PathItem)
@@ -375,9 +406,9 @@ makeRootPathItem = ("/", p)
pr = (mempty :: PathItem) & get ?~ getOp pr = (mempty :: PathItem) & get ?~ getOp
p = pr p = pr
makePathItems :: [Routine] -> [Table] -> InsOrdHashMap FilePath PathItem makePathItems :: [Routine] -> [(Table, TableAccess)] -> InsOrdHashMap FilePath PathItem
makePathItems pds ti = fromList $ makeRootPathItem : makePathItems pds tis = fromList $ makeRootPathItem :
fmap makePathItem ti ++ fmap makeProcPathItem pds fmap makePathItem tis ++ fmap makeProcPathItem pds
makeSecurityDefinitions :: Text -> Bool -> SecurityDefinitions makeSecurityDefinitions :: Text -> Bool -> SecurityDefinitions
makeSecurityDefinitions secName allow makeSecurityDefinitions secName allow
@@ -387,8 +418,8 @@ makeSecurityDefinitions secName allow
secSchType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader) secSchType = SecuritySchemeApiKey (ApiKeyParams "Authorization" ApiKeyHeader)
secSchDescription = Just "Add the token prepending \"Bearer \" (without quotes) to it" secSchDescription = Just "Add the token prepending \"Bearer \" (without quotes) to it"
postgrestSpec :: (Text, Text) -> RelationshipsMap -> [Routine] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger postgrestSpec :: (Text, Text) -> RelationshipsMap -> [Routine] -> [(Table, TableAccess)] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger
postgrestSpec (prettyVersion, docsVersion) rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger) postgrestSpec (prettyVersion, docsVersion) rels pds tis (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)
& basePath ?~ T.unpack b & basePath ?~ T.unpack b
& schemes ?~ [s'] & schemes ?~ [s']
& info .~ ((mempty :: Info) & info .~ ((mempty :: Info)
@@ -399,9 +430,9 @@ postgrestSpec (prettyVersion, docsVersion) rels pds ti (s, h, p, b) sd allowSecu
& description ?~ "PostgREST Documentation" & description ?~ "PostgREST Documentation"
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html")) & url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html"))
& host .~ h' & host .~ h'
& definitions .~ fromList (makeTableDef rels <$> ti) & definitions .~ fromList (makeTableDef rels <$> tis)
& parameters .~ fromList (makeParamDefs ti) & parameters .~ fromList (makeParamDefs rels tis)
& paths .~ makePathItems pds ti & paths .~ makePathItems pds tis
& produces .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV] & produces .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]
& consumes .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV] & consumes .~ makeMimeList [MTApplicationJSON, MTVndSingularJSON True, MTVndSingularJSON False, MTTextCSV]
& securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef & securityDefinitions .~ makeSecurityDefinitions securityDefName allowSecurityDef
+52
View File
@@ -222,6 +222,58 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do
. nth 0 . nth 0
liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|] liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]
it "reflects table privileges in the HTTP methods" $ do
r <- simpleBody <$> get "/"
let selectonlyGet = r ^? key "paths" . key "/selectonly" . key "get"
selectonlyPost = r ^? key "paths" . key "/selectonly" . key "post"
insertonlyGet = r ^? key "paths" . key "/insertonly" . key "get"
insertonlyPost = r ^? key "paths" . key "/insertonly" . key "post"
insertonlyDelete = r ^? key "paths" . key "/insertonly" . key "delete"
limitedStarsGet = r ^? key "paths" . key "/limited_article_stars" . key "get"
limitedStarsPost = r ^? key "paths" . key "/limited_article_stars" . key "post"
limitedStarsPatch = r ^? key "paths" . key "/limited_article_stars" . key "patch"
limitedStarsDelete = r ^? key "paths" . key "/limited_article_stars" . key "delete"
liftIO $ do
selectonlyGet `shouldNotBe` Nothing
selectonlyPost `shouldBe` Nothing
insertonlyGet `shouldBe` Nothing
insertonlyPost `shouldNotBe` Nothing
insertonlyDelete `shouldBe` Nothing
limitedStarsGet `shouldNotBe` Nothing
limitedStarsPost `shouldNotBe` Nothing
limitedStarsPatch `shouldNotBe` Nothing
limitedStarsDelete `shouldBe` Nothing
it "reflects column privileges in the table definition" $ do
r <- simpleBody <$> get "/"
let appUsersId = r ^? key "definitions" . key "app_users" . key "properties" . key "id"
appUsersEmail = r ^? key "definitions" . key "app_users" . key "properties" . key "email"
appUsersPassword = r ^? key "definitions" . key "app_users" . key "properties" . key "password"
appUsersRequired = r ^? key "definitions" . key "app_users" . key "required"
liftIO $ do
appUsersId `shouldNotBe` Nothing
appUsersEmail `shouldNotBe` Nothing
appUsersPassword `shouldBe` Nothing
appUsersRequired `shouldBe` Just [aesonQQ|["id", "email"]|]
it "reflects column privileges in the rowFilter parameters" $ do
r <- simpleBody <$> get "/"
let filterId = r ^? key "parameters" . key "rowFilter.app_users.id"
filterEmail = r ^? key "parameters" . key "rowFilter.app_users.email"
filterPassword = r ^? key "parameters" . key "rowFilter.app_users.password"
liftIO $ do
filterId `shouldNotBe` Nothing
filterEmail `shouldNotBe` Nothing
filterPassword `shouldBe` Nothing
it "includes a fk description for a O2O relationship" $ do it "includes a fk description for a O2O relationship" $ do
r <- simpleBody <$> get "/" r <- simpleBody <$> get "/"
+3
View File
@@ -28,10 +28,13 @@ REVOKE ALL PRIVILEGES ON TABLE
, authors_only , authors_only
, insertonly , insertonly
, limited_article_stars , limited_article_stars
, selectonly
FROM postgrest_test_anonymous; FROM postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous; GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
GRANT SELECT ON TABLE selectonly TO postgrest_test_anonymous;
GRANT USAGE ON SEQUENCE GRANT USAGE ON SEQUENCE
auto_incrementing_pk_id_seq auto_incrementing_pk_id_seq
, items_id_seq , items_id_seq
+5
View File
@@ -1926,6 +1926,11 @@ create table app_users (
password text not null password text not null
); );
create table selectonly (
id integer primary key,
name text
);
create table private.pages ( create table private.pages (
link int not null unique link int not null unique
, url text , url text