restrict openapi spec based on sql grants
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 <$> tableColumnsList t)
|
& properties .~ fromList (makeProperty t rels <$> cols)
|
||||||
& required .~ fmap colName (filter (not . colNullable) $ tableColumnsList t))
|
& required .~ fmap colName (filter (not . colNullable) cols))
|
||||||
|
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))
|
||||||
|
, ("body." <> tn <> ".patch", makeBodyParam (taUpdateCols access))
|
||||||
|
]
|
||||||
|
where
|
||||||
|
tn = tableName t
|
||||||
|
makeBodyParam cols = (mempty :: Param)
|
||||||
& name .~ tn
|
& name .~ tn
|
||||||
& description ?~ tn
|
& description ?~ tn
|
||||||
& required ?~ False
|
& required ?~ False
|
||||||
& schema .~ ParamBody (Ref (Reference tn)))
|
& 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
|
||||||
|
|||||||
@@ -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 "/"
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
@@ -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
|
||||||
|
|||||||
Vendored
+5
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user