Compare commits
14
Commits
feat/openapi
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02d83d1c01 | ||
|
|
e89e0bc255 | ||
|
|
9e20e5df90 | ||
|
|
2122dcef97 | ||
|
|
d84d00be8e | ||
|
|
3a034fcc2a | ||
|
|
a2a592a617 | ||
|
|
89fe63fce0 | ||
|
|
f21949a517 | ||
|
|
6ead34e70b | ||
|
|
e3206262d2 | ||
|
|
a3f36d6184 | ||
|
|
6cc1698c05 | ||
|
|
77d6055683 |
@@ -4,10 +4,6 @@ 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
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ source_suffix = ".rst"
|
|||||||
master_doc = "index"
|
master_doc = "index"
|
||||||
|
|
||||||
# This is overridden by readthedocs with the version tag anyway
|
# This is overridden by readthedocs with the version tag anyway
|
||||||
version = "16"
|
version = "devel"
|
||||||
# To avoid repetition in <title> we set this to an empty string.
|
# To avoid repetition in <title> we set this to an empty string.
|
||||||
release = ""
|
release = ""
|
||||||
|
|
||||||
|
|||||||
@@ -31,60 +31,60 @@ This section talks briefly about various important modules.
|
|||||||
Main
|
Main
|
||||||
----
|
----
|
||||||
|
|
||||||
The starting point of the program is `Main.hs <https://github.com/PostgREST/postgrest/blob/v16/src/executable/Main.hs>`_.
|
The starting point of the program is `Main.hs <https://github.com/PostgREST/postgrest/blob/main/src/executable/Main.hs>`_.
|
||||||
|
|
||||||
CLI
|
CLI
|
||||||
---
|
---
|
||||||
|
|
||||||
Main then calls `CLI.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/CLI.hs>`_, which is in charge of :ref:`cli`.
|
Main then calls `CLI.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/CLI.hs>`_, which is in charge of :ref:`cli`.
|
||||||
|
|
||||||
App
|
App
|
||||||
---
|
---
|
||||||
|
|
||||||
`App.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/App.hs>`_ is then in charge of composing the different modules.
|
`App.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/App.hs>`_ is then in charge of composing the different modules.
|
||||||
|
|
||||||
Auth
|
Auth
|
||||||
----
|
----
|
||||||
|
|
||||||
`Auth.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/Auth.hs>`_ is in charge of :ref:`authn`.
|
`Auth.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Auth.hs>`_ is in charge of :ref:`authn`.
|
||||||
|
|
||||||
Api Request
|
Api Request
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
`ApiRequest.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/ApiRequest.hs>`_ is in charge of parsing the URL query string (following PostgREST syntax), the request headers, and the request body.
|
`ApiRequest.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/ApiRequest.hs>`_ is in charge of parsing the URL query string (following PostgREST syntax), the request headers, and the request body.
|
||||||
|
|
||||||
A request might be rejected at this level if it's invalid. For example when providing an unknown media type to PostgREST or using an unknown HTTP method.
|
A request might be rejected at this level if it's invalid. For example when providing an unknown media type to PostgREST or using an unknown HTTP method.
|
||||||
|
|
||||||
Plan
|
Plan
|
||||||
----
|
----
|
||||||
|
|
||||||
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
|
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
|
||||||
|
|
||||||
A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.
|
A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.
|
||||||
|
|
||||||
Query
|
Query
|
||||||
-----
|
-----
|
||||||
|
|
||||||
`Query.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/Query.hs>`_ generates the SQL queries (parametrized and prepared) required to satisfy the user request.
|
`Query.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Query.hs>`_ generates the SQL queries (parametrized and prepared) required to satisfy the user request.
|
||||||
|
|
||||||
Only at this stage a connection from the pool might be used.
|
Only at this stage a connection from the pool might be used.
|
||||||
|
|
||||||
Schema Cache
|
Schema Cache
|
||||||
------------
|
------------
|
||||||
|
|
||||||
`SchemaCache.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/SchemaCache.hs>`_ is in charge of :ref:`schema_cache`.
|
`SchemaCache.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/SchemaCache.hs>`_ is in charge of :ref:`schema_cache`.
|
||||||
|
|
||||||
Config
|
Config
|
||||||
------
|
------
|
||||||
|
|
||||||
`Config.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/Config.hs>`_ is in charge of :ref:`configuration`.
|
`Config.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Config.hs>`_ is in charge of :ref:`configuration`.
|
||||||
|
|
||||||
Admin
|
Admin
|
||||||
-----
|
-----
|
||||||
|
|
||||||
`Admin.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/Admin.hs>`_ is in charge of the :ref:`admin_server`.
|
`Admin.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/Admin.hs>`_ is in charge of the :ref:`admin_server`.
|
||||||
|
|
||||||
Listener
|
Listener
|
||||||
--------
|
--------
|
||||||
|
|
||||||
`Reload.hs <https://github.com/PostgREST/postgrest/blob/v16/src/library/PostgREST/AppState/Reload.hs>`_ is in charge of the :ref:`listener`.
|
`Reload.hs <https://github.com/PostgREST/postgrest/blob/main/src/library/PostgREST/AppState/Reload.hs>`_ is in charge of the :ref:`listener`.
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ 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
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ let
|
|||||||
echo "Updating docs/conf.py ..."
|
echo "Updating docs/conf.py ..."
|
||||||
sed -i -E "s/^(version = ).*$/\1\"$new_docs_version\"/" docs/conf.py > /dev/null
|
sed -i -E "s/^(version = ).*$/\1\"$new_docs_version\"/" docs/conf.py > /dev/null
|
||||||
|
|
||||||
git add postgrest.cabal docs/conf.py > /dev/null
|
echo "Updating Haskell source file links ..."
|
||||||
|
sed -i -E "s#(github\.com/PostgREST/postgrest/blob)/main/#\1/$new_version/#g" docs/explanations/architecture.rst
|
||||||
|
|
||||||
|
git add postgrest.cabal docs/conf.py docs/explanations/architecture.rst > /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,6 @@
|
|||||||
cabal-version: 3.0
|
cabal-version: 3.0
|
||||||
name: postgrest
|
name: postgrest
|
||||||
version: 16.1
|
version: 17
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
@@ -78,7 +78,6 @@ 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
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ import Network.Wai.Handler.Warp (defaultSettings, setBeforeMainLoop, setHost,
|
|||||||
setOnException, setPort, setServerName)
|
setOnException, setPort, setServerName)
|
||||||
|
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Decoders as HD
|
|
||||||
import qualified Hasql.DynamicStatements.Statement as SQL
|
|
||||||
import qualified Hasql.Transaction as SQL
|
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import qualified Network.Wai.Handler.Warp as Warp
|
import qualified Network.Wai.Handler.Warp as Warp
|
||||||
import qualified Network.Wai.Header as WaiHeader
|
import qualified Network.Wai.Header as WaiHeader
|
||||||
@@ -52,7 +48,6 @@ import qualified PostgREST.Response as Response
|
|||||||
import qualified PostgREST.Unix as Unix (installSignalHandlers)
|
import qualified PostgREST.Unix as Unix (installSignalHandlers)
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..))
|
import PostgREST.ApiRequest (ApiRequest (..))
|
||||||
import PostgREST.ApiRequest.Types (Action (..), DbAction (..))
|
|
||||||
import PostgREST.AppState (AppState)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.AppState.Reload (runListener)
|
import PostgREST.AppState.Reload (runListener)
|
||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Auth.Types (AuthResult (..))
|
||||||
@@ -60,8 +55,6 @@ import PostgREST.Config (AppConfig (..))
|
|||||||
import PostgREST.Error (Error)
|
import PostgREST.Error (Error)
|
||||||
import PostgREST.Network (resolveSocketToAddress)
|
import PostgREST.Network (resolveSocketToAddress)
|
||||||
import PostgREST.Observation (Observation (..))
|
import PostgREST.Observation (Observation (..))
|
||||||
import PostgREST.Query.OpenApi (TablesAccess, tablesAccessStatement)
|
|
||||||
import PostgREST.Query.SqlFragment (setConfigWithConstantName)
|
|
||||||
import PostgREST.Response.Performance (ServerTiming (..), serverTimingHeader)
|
import PostgREST.Response.Performance (ServerTiming (..), serverTimingHeader)
|
||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.TimeIt (timeItT)
|
import PostgREST.TimeIt (timeItT)
|
||||||
@@ -214,8 +207,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
|||||||
body <- liftIO $ Wai.strictRequestBody req
|
body <- liftIO $ Wai.strictRequestBody req
|
||||||
|
|
||||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
|
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
|
||||||
tableAccess <- liftIO $ getTablesAccess appState apiReq authResult
|
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||||
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq tableAccess sCache
|
|
||||||
|
|
||||||
let warnings = Plan.legacyWarnings plan
|
let warnings = Plan.legacyWarnings plan
|
||||||
legacyWarnMsg = "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release."
|
legacyWarnMsg = "Embedded resource was referenced by relation name even though it has an alias. This is deprecated and will stop working in a future release."
|
||||||
@@ -277,31 +269,6 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
|||||||
in
|
in
|
||||||
[(hWarning, "299 " <> pgrstVer <> " \"" <> encodeUtf8 warnMsg <> "\"")]
|
[(hWarning, "299 " <> pgrstVer <> " \"" <> encodeUtf8 warnMsg <> "\"")]
|
||||||
|
|
||||||
-- | Fetch the privileges the request role has on the tables of the requested
|
|
||||||
-- schema, so that the planner can restrict the default "select *" to the
|
|
||||||
-- columns the role can actually read. Returns an empty map when the request
|
|
||||||
-- doesn't need it or when the query fails (degrading to the previous behavior).
|
|
||||||
getTablesAccess :: AppState -> ApiRequest -> AuthResult -> IO TablesAccess
|
|
||||||
getTablesAccess appState ApiRequest{iAction, iSchema} AuthResult{authRole} =
|
|
||||||
case iAction of
|
|
||||||
ActDb ActRelationRead{} -> query
|
|
||||||
ActDb ActRelationMut{} -> query
|
|
||||||
ActDb ActRoutine{} -> query
|
|
||||||
_ -> pure mempty
|
|
||||||
where
|
|
||||||
query = do
|
|
||||||
result <- AppState.usePool appState $
|
|
||||||
SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ do
|
|
||||||
SQL.statement mempty (roleStatement authRole)
|
|
||||||
SQL.statement mempty (tablesAccessStatement iSchema)
|
|
||||||
pure $ fromRight mempty result
|
|
||||||
|
|
||||||
roleStatement role =
|
|
||||||
SQL.dynamicallyParameterized
|
|
||||||
("select " <> setConfigWithConstantName ("role", role))
|
|
||||||
HD.noResult
|
|
||||||
False
|
|
||||||
|
|
||||||
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
|
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
|
||||||
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
|
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
|
||||||
then do
|
then do
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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)
|
||||||
@@ -43,7 +44,6 @@ import PostgREST.Plan (ActionPlan (..), CrudPlan (..),
|
|||||||
DbActionPlan (..), InfoPlan (..),
|
DbActionPlan (..), InfoPlan (..),
|
||||||
InspectPlan (..))
|
InspectPlan (..))
|
||||||
import PostgREST.Query (MainQuery (..))
|
import PostgREST.Query (MainQuery (..))
|
||||||
import PostgREST.Query.OpenApi (TablesAccess, decodeTablesAccess)
|
|
||||||
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, TablesAccess, RoutineMap, Maybe Text))
|
| MaybeDbResult InspectPlan (Maybe (TablesMap, 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,25 +174,34 @@ 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 decodeTablesAccess configDbPreparedStatements
|
tableAccess <- SQL.statement mempty $ SQL.dynamicallyParameterized tblsQ decodeAccessibleIdentifiers 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 _ -> HM.member qi tableAccess) $ SchemaCache.dbTables sCache
|
let tbls = HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache
|
||||||
|
|
||||||
pure $ MaybeDbResult plan (Just (tbls, tableAccess, accFuncs, schDesc))
|
pure $ MaybeDbResult plan (Just (tbls, 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, mempty, routs, schDesc))
|
pure $ MaybeDbResult plan (Just (tbls, 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)
|
||||||
|
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
|
-- 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,
|
||||||
-- PUT /items?id=eq.14 { "id" : 2, .. } is rejected.
|
-- PUT /items?id=eq.14 { "id" : 2, .. } is rejected.
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ import PostgREST.Error (ApiRequestError (..), Error (..),
|
|||||||
SchemaCacheError (..))
|
SchemaCacheError (..))
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.Plan.Negotiate (negotiateContent)
|
import PostgREST.Plan.Negotiate (negotiateContent)
|
||||||
import PostgREST.Query.OpenApi (TableAccess (..), TablesAccess)
|
|
||||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
convertToLimitZeroRange,
|
convertToLimitZeroRange,
|
||||||
@@ -168,23 +167,23 @@ readPlanWarning :: ReadPlan -> Maybe (Text, Text)
|
|||||||
readPlanWarning ReadPlan{relName, relAlias = Just alias, relIsLegacyTargetNameMatch = True} = Just (relName, alias)
|
readPlanWarning ReadPlan{relName, relAlias = Just alias, relIsLegacyTargetNameMatch = True} = Just (relName, alias)
|
||||||
readPlanWarning _ = Nothing
|
readPlanWarning _ = Nothing
|
||||||
|
|
||||||
actionPlan :: Action -> AppConfig -> ApiRequest -> TablesAccess -> SchemaCache -> Either Error ActionPlan
|
actionPlan :: Action -> AppConfig -> ApiRequest -> SchemaCache -> Either Error ActionPlan
|
||||||
actionPlan act conf apiReq tAccess sCache = case act of
|
actionPlan act conf apiReq sCache = case act of
|
||||||
ActDb dbAct -> Db <$> dbActionPlan dbAct conf apiReq tAccess sCache
|
ActDb dbAct -> Db <$> dbActionPlan dbAct conf apiReq sCache
|
||||||
ActRelationInfo ident -> pure . NoDb $ RelInfoPlan ident
|
ActRelationInfo ident -> pure . NoDb $ RelInfoPlan ident
|
||||||
ActRoutineInfo ident inv ->
|
ActRoutineInfo ident inv ->
|
||||||
let crPln = callReadPlan ident conf tAccess sCache apiReq inv in
|
let crPln = callReadPlan ident conf sCache apiReq inv in
|
||||||
NoDb . RoutineInfoPlan . crProc <$> crPln
|
NoDb . RoutineInfoPlan . crProc <$> crPln
|
||||||
ActSchemaInfo -> pure $ NoDb SchemaInfoPlan
|
ActSchemaInfo -> pure $ NoDb SchemaInfoPlan
|
||||||
|
|
||||||
dbActionPlan :: DbAction -> AppConfig -> ApiRequest -> TablesAccess -> SchemaCache -> Either Error DbActionPlan
|
dbActionPlan :: DbAction -> AppConfig -> ApiRequest -> SchemaCache -> Either Error DbActionPlan
|
||||||
dbActionPlan dbAct conf apiReq tAccess sCache = case dbAct of
|
dbActionPlan dbAct conf apiReq sCache = case dbAct of
|
||||||
ActRelationRead identifier headersOnly ->
|
ActRelationRead identifier headersOnly ->
|
||||||
toDbActPlan <$> wrappedReadPlan identifier conf tAccess sCache apiReq headersOnly
|
toDbActPlan <$> wrappedReadPlan identifier conf sCache apiReq headersOnly
|
||||||
ActRelationMut identifier mut ->
|
ActRelationMut identifier mut ->
|
||||||
toDbActPlan <$> mutateReadPlan mut apiReq identifier conf tAccess sCache
|
toDbActPlan <$> mutateReadPlan mut apiReq identifier conf sCache
|
||||||
ActRoutine identifier invMethod ->
|
ActRoutine identifier invMethod ->
|
||||||
toDbActPlan <$> callReadPlan identifier conf tAccess sCache apiReq invMethod
|
toDbActPlan <$> callReadPlan identifier conf sCache apiReq invMethod
|
||||||
ActSchemaRead tSchema headersOnly ->
|
ActSchemaRead tSchema headersOnly ->
|
||||||
MayUseDb <$> inspectPlan apiReq headersOnly tSchema
|
MayUseDb <$> inspectPlan apiReq headersOnly tSchema
|
||||||
where
|
where
|
||||||
@@ -192,32 +191,32 @@ dbActionPlan dbAct conf apiReq tAccess sCache = case dbAct of
|
|||||||
MTVndPlan{} -> DbCrud True pl
|
MTVndPlan{} -> DbCrud True pl
|
||||||
_ -> DbCrud False pl
|
_ -> DbCrud False pl
|
||||||
|
|
||||||
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> TablesAccess -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
|
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
|
||||||
wrappedReadPlan identifier conf tAccess sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
||||||
qi <- findTable identifier sCache
|
qi <- findTable identifier sCache
|
||||||
rPlan <- readPlan qi conf tAccess sCache apiRequest
|
rPlan <- readPlan qi conf sCache apiRequest
|
||||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||||
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
|
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
|
||||||
|
|
||||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> TablesAccess -> SchemaCache -> Either Error CrudPlan
|
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
||||||
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf tAccess sCache = do
|
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
||||||
qi <- findTable identifier sCache
|
qi <- findTable identifier sCache
|
||||||
rPlan <- readPlan qi conf tAccess sCache apiRequest
|
rPlan <- readPlan qi conf sCache apiRequest
|
||||||
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
||||||
|
|
||||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> TablesAccess -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan
|
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan
|
||||||
callReadPlan identifier conf tAccess sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do
|
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do
|
||||||
let paramKeys = case invMethod of
|
let paramKeys = case invMethod of
|
||||||
InvRead _ -> S.fromList $ fst <$> qsParams'
|
InvRead _ -> S.fromList $ fst <$> qsParams'
|
||||||
Inv -> iColumns
|
Inv -> iColumns
|
||||||
proc@Function{..} <- mapLeft SchemaCacheErr $
|
proc@Function{..} <- mapLeft SchemaCacheErr $
|
||||||
findProc identifier paramKeys (dbRoutines sCache) iContentMediaType (invMethod == Inv)
|
findProc identifier paramKeys (dbRoutines sCache) iContentMediaType (invMethod == Inv)
|
||||||
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
|
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
|
||||||
rPlan <- readPlan relIdentifier conf tAccess sCache apiRequest
|
rPlan <- readPlan relIdentifier conf sCache apiRequest
|
||||||
let args = case (invMethod, iContentMediaType) of
|
let args = case (invMethod, iContentMediaType) of
|
||||||
(InvRead _, _) -> DirectArgs $ toRpcParams proc qsParams'
|
(InvRead _, _) -> DirectArgs $ toRpcParams proc qsParams'
|
||||||
(Inv, MTUrlEncoded) -> DirectArgs $ maybe mempty (toRpcParams proc . payArray) iPayload
|
(Inv, MTUrlEncoded) -> DirectArgs $ maybe mempty (toRpcParams proc . payArray) iPayload
|
||||||
@@ -314,11 +313,10 @@ data ResolverContext = ResolverContext
|
|||||||
, representations :: RepresentationsMap
|
, representations :: RepresentationsMap
|
||||||
, qi :: QualifiedIdentifier -- ^ The table we're currently attending; changes as we recurse into joins etc.
|
, qi :: QualifiedIdentifier -- ^ The table we're currently attending; changes as we recurse into joins etc.
|
||||||
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
|
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
|
||||||
, tablesAccess :: TablesAccess -- ^ Privileges the request role has on the exposed tables.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField
|
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField
|
||||||
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) (colType col) Nothing (colDefault col) False Nothing
|
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) (colType col) Nothing (colDefault col) False
|
||||||
|
|
||||||
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField
|
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField
|
||||||
resolveTableFieldName table fieldName toTsV=
|
resolveTableFieldName table fieldName toTsV=
|
||||||
@@ -378,11 +376,11 @@ resolveQueryInputField ctx field opExpr = withTextParse ctx $ resolveTypeOrUnkno
|
|||||||
-- | Builds the ReadPlan tree on a number of stages.
|
-- | Builds the ReadPlan tree on a number of stages.
|
||||||
-- | Adds filters, order, limits on its respective nodes.
|
-- | Adds filters, order, limits on its respective nodes.
|
||||||
-- | Adds joins conditions obtained from resource embedding.
|
-- | Adds joins conditions obtained from resource embedding.
|
||||||
readPlan :: QualifiedIdentifier -> AppConfig -> TablesAccess -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
|
readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
|
||||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates, configUrlUseLegacyTargetNames} tAccess SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
|
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates, configUrlUseLegacyTargetNames} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
|
||||||
let
|
let
|
||||||
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
|
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
|
||||||
ctx = ResolverContext dbTables dbRepresentations qi "json" tAccess
|
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||||
in
|
in
|
||||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||||
addToManyOrderSelects =<<
|
addToManyOrderSelects =<<
|
||||||
@@ -478,8 +476,7 @@ knownColumnsInContext ResolverContext{..} =
|
|||||||
-- | Expand "select *" into explicit field names of the table in the following situations:
|
-- | Expand "select *" into explicit field names of the table in the following situations:
|
||||||
-- * When there are data representations present.
|
-- * When there are data representations present.
|
||||||
-- * When there is an aggregate function in a given ReadPlan or its parent.
|
-- * When there is an aggregate function in a given ReadPlan or its parent.
|
||||||
-- * When the ReadPlan is a to-many spread relationship.
|
-- * When the ReadPlan is a to-many spread relationship
|
||||||
-- * When the default select(when no "select" is given) would include columns the request role cannot read.
|
|
||||||
expandStars :: ResolverContext -> ReadPlanTree -> Either Error ReadPlanTree
|
expandStars :: ResolverContext -> ReadPlanTree -> Either Error ReadPlanTree
|
||||||
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
||||||
where
|
where
|
||||||
@@ -499,16 +496,13 @@ expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
|||||||
adjustContext context fromQI _ = context{qi=fromQI}
|
adjustContext context fromQI _ = context{qi=fromQI}
|
||||||
|
|
||||||
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
|
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
|
||||||
expandStarsForTable ctx@ResolverContext{representations, outputType, tables, qi} hasAgg rp@ReadPlan{select=selectFields, relSpread=spread}
|
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields, relSpread=spread}
|
||||||
-- We expand the '*' select if either of the below are true:
|
-- We expand if either of the below are true:
|
||||||
-- * The target table has columns the request role cannot read.
|
-- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.
|
||||||
-- * There is an aggregate function in this ReadPlan's sub-tree.
|
-- * We have a '*' select AND the target table has at least one data representation.
|
||||||
-- * The target table has at least one data representation.
|
|
||||||
-- We ignore '*' selects that have an aggregate function attached, unless it's a `COUNT(*)` for a Spread Embed,
|
-- We ignore '*' selects that have an aggregate function attached, unless it's a `COUNT(*)` for a Spread Embed,
|
||||||
-- we tag it as "full row" in that case.
|
-- we tag it as "full row" in that case.
|
||||||
| hasStarSelect && hasLimitedPrivileges = rp{select = concatMap (expandStarSelectField (isJust spread) accessibleColumns) selectFields <> rlsFields}
|
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField (isJust spread) knownColumns) selectFields}
|
||||||
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField (isJust spread) knownColumns) selectFields <> rlsFields}
|
|
||||||
| hasStarSelect = rp{select = selectFields <> rlsFields}
|
|
||||||
| otherwise = rp
|
| otherwise = rp
|
||||||
where
|
where
|
||||||
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
|
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
|
||||||
@@ -516,11 +510,6 @@ expandStarsForTable ctx@ResolverContext{representations, outputType, tables, qi}
|
|||||||
shouldExpandOrTag aggFunc = isNothing aggFunc || (isJust spread && aggFunc == Just Count)
|
shouldExpandOrTag aggFunc = isNothing aggFunc || (isJust spread && aggFunc == Just Count)
|
||||||
hasDataRepresentation = any hasOutputRep knownColumns
|
hasDataRepresentation = any hasOutputRep knownColumns
|
||||||
knownColumns = knownColumnsInContext ctx
|
knownColumns = knownColumnsInContext ctx
|
||||||
hasLimitedPrivileges = accessibleColumns /= knownColumns
|
|
||||||
accessibleColumns = accessibleColumnsInContext ctx
|
|
||||||
rlsFields = case HM.lookup qi tables of
|
|
||||||
Just tbl -> rlsSelectFields tbl
|
|
||||||
Nothing -> []
|
|
||||||
|
|
||||||
hasOutputRep :: Column -> Bool
|
hasOutputRep :: Column -> Bool
|
||||||
hasOutputRep col = HM.member (colNominalType col, outputType) representations
|
hasOutputRep col = HM.member (colNominalType col, outputType) representations
|
||||||
@@ -532,33 +521,6 @@ expandStarsForTable ctx@ResolverContext{representations, outputType, tables, qi}
|
|||||||
[sel { csField = fld { cfFullRow = True } }]
|
[sel { csField = fld { cfFullRow = True } }]
|
||||||
expandStarSelectField _ _ selectField = [selectField]
|
expandStarSelectField _ _ selectField = [selectField]
|
||||||
|
|
||||||
-- | Synthetic can_edit/can_delete select fields for a table that has row-level
|
|
||||||
-- security policies restricting UPDATE/DELETE. These carry the raw qualifier
|
|
||||||
-- expression and are rendered as computed columns on `select *`.
|
|
||||||
rlsSelectFields :: Table -> [CoercibleSelectField]
|
|
||||||
rlsSelectFields tbl = catMaybes [rlsField "can_edit" (tableRlsEditQual tbl), rlsField "can_delete" (tableRlsDeleteQual tbl)]
|
|
||||||
where
|
|
||||||
rlsField :: FieldName -> Maybe Text -> Maybe CoercibleSelectField
|
|
||||||
rlsField name qual = do
|
|
||||||
expr <- qual
|
|
||||||
pure CoercibleSelectField
|
|
||||||
{ csField = (unknownField name []) { cfIRType = "boolean", cfBaseType = "boolean", cfExpression = Just expr }
|
|
||||||
, csAggFunction = Nothing
|
|
||||||
, csAggCast = Nothing
|
|
||||||
, csCast = Nothing
|
|
||||||
, csAlias = Just name
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | The columns of the current table that the request role can SELECT. Falls
|
|
||||||
-- back to all known columns when no access info is available or the role has
|
|
||||||
-- no SELECT privilege on any column.
|
|
||||||
accessibleColumnsInContext :: ResolverContext -> [Column]
|
|
||||||
accessibleColumnsInContext ctx@ResolverContext{qi=tblQi, tablesAccess} =
|
|
||||||
case HM.lookup tblQi tablesAccess of
|
|
||||||
Just (TableAccess selCols _ _ _) | not (null selCols) ->
|
|
||||||
filter (\col -> colName col `elem` selCols) (knownColumnsInContext ctx)
|
|
||||||
_ -> knownColumnsInContext ctx
|
|
||||||
|
|
||||||
-- | Enforces the `max-rows` config on the result
|
-- | Enforces the `max-rows` config on the result
|
||||||
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either Error ReadPlanTree
|
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either Error ReadPlanTree
|
||||||
treeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request
|
treeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request
|
||||||
@@ -968,7 +930,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- where_ = [
|
-- where_ = [
|
||||||
-- CoercibleStmnt (
|
-- CoercibleStmnt (
|
||||||
-- CoercibleFilter {
|
-- CoercibleFilter {
|
||||||
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False, cfExpression = Nothing},
|
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
||||||
-- opExpr = op
|
-- opExpr = op
|
||||||
-- }
|
-- }
|
||||||
-- )
|
-- )
|
||||||
@@ -985,7 +947,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
||||||
--
|
--
|
||||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
||||||
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False, cfExpression = Nothing}, opExpr = OpExpr True (Is IsNull)})]
|
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
||||||
--
|
--
|
||||||
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
||||||
--
|
--
|
||||||
@@ -1097,7 +1059,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
Left $ ApiRequestErr InvalidFilters
|
Left $ ApiRequestErr InvalidFilters
|
||||||
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
||||||
where
|
where
|
||||||
ctx = ResolverContext dbTables dbRepresentations qi "json" mempty
|
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||||
confCols = fromMaybe pkCols qsOnConflict
|
confCols = fromMaybe pkCols qsOnConflict
|
||||||
QueryParams.QueryParams{..} = iQueryParams
|
QueryParams.QueryParams{..} = iQueryParams
|
||||||
returnings =
|
returnings =
|
||||||
|
|||||||
@@ -47,11 +47,10 @@ data CoercibleField = CoercibleField
|
|||||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||||
, cfDefault :: Maybe Text
|
, cfDefault :: Maybe Text
|
||||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spread resources.
|
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spread resources.
|
||||||
, cfExpression :: Maybe Text -- ^ Raw SQL expression for a computed field (e.g. RLS-derived can_edit/can_delete). When present the field is rendered as this expression instead of a table column.
|
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||||
unknownField name path = CoercibleField name path False Nothing "" "" Nothing Nothing False Nothing
|
unknownField name path = CoercibleField name path False Nothing "" "" Nothing Nothing False
|
||||||
|
|
||||||
-- | Like an API request LogicTree, but with coercible field information.
|
-- | Like an API request LogicTree, but with coercible field information.
|
||||||
data CoercibleLogicTree
|
data CoercibleLogicTree
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
{-|
|
|
||||||
Module : PostgREST.Query.OpenApi
|
|
||||||
Description : Types for reflecting the role privileges on the OpenAPI output.
|
|
||||||
-}
|
|
||||||
module PostgREST.Query.OpenApi
|
|
||||||
( TableAccess (..)
|
|
||||||
, TablesAccess
|
|
||||||
, tablesAccessStatement
|
|
||||||
, decodeTablesAccess
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
import qualified Hasql.Decoders as HD
|
|
||||||
import qualified Hasql.DynamicStatements.Statement as SQL
|
|
||||||
import qualified Hasql.Statement as SQL
|
|
||||||
|
|
||||||
import qualified PostgREST.Query.SqlFragment as SqlFragment
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
-- | Statement that returns the privileges the current role has on each
|
|
||||||
-- accessible relation of the given schema.
|
|
||||||
tablesAccessStatement :: Text -> SQL.Statement () TablesAccess
|
|
||||||
tablesAccessStatement schema =
|
|
||||||
SQL.dynamicallyParameterized (SqlFragment.accessibleTables schema) decodeTablesAccess False
|
|
||||||
|
|
||||||
decodeTablesAccess :: HD.Result TablesAccess
|
|
||||||
decodeTablesAccess =
|
|
||||||
let
|
|
||||||
row = (,) <$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
|
|
||||||
<*> (TableAccess
|
|
||||||
<$> arrayColumn HD.text
|
|
||||||
<*> arrayColumn HD.text
|
|
||||||
<*> arrayColumn HD.text
|
|
||||||
<*> column HD.bool)
|
|
||||||
in
|
|
||||||
HM.fromList <$> HD.rowList row
|
|
||||||
|
|
||||||
column :: HD.Value a -> HD.Row a
|
|
||||||
column = HD.column . HD.nonNullable
|
|
||||||
|
|
||||||
arrayColumn :: HD.Value a -> HD.Row [a]
|
|
||||||
arrayColumn = column . HD.listArray . HD.nonNullable
|
|
||||||
@@ -180,7 +180,7 @@ callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScal
|
|||||||
KeyParams [] -> "FROM " <> callIt mempty
|
KeyParams [] -> "FROM " <> callIt mempty
|
||||||
KeyParams prms -> case arguments of
|
KeyParams prms -> case arguments of
|
||||||
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
||||||
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) mempty Nothing Nothing False Nothing) <$> prms) False True False <> ", " <>
|
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) mempty Nothing Nothing False) <$> prms) False True False <> ", " <>
|
||||||
"LATERAL " <> callIt (fmtParams prms)
|
"LATERAL " <> callIt (fmtParams prms)
|
||||||
|
|
||||||
callIt :: SQL.Snippet -> SQL.Snippet
|
callIt :: SQL.Snippet -> SQL.Snippet
|
||||||
|
|||||||
@@ -249,7 +249,6 @@ pgFmtField table cf = case cfToTsVector cf of
|
|||||||
_ -> fmtFld
|
_ -> fmtFld
|
||||||
where
|
where
|
||||||
fmtFld = case cf of
|
fmtFld = case cf of
|
||||||
CoercibleField{cfExpression=Just expr} -> SQL.sql (encodeUtf8 expr)
|
|
||||||
CoercibleField{cfFullRow=True} -> pgFmtIdent (qiName table)
|
CoercibleField{cfFullRow=True} -> pgFmtIdent (qiName table)
|
||||||
CoercibleField{cfName=fn, cfJsonPath=[]} -> pgFmtColumn table fn
|
CoercibleField{cfName=fn, cfJsonPath=[]} -> pgFmtColumn table fn
|
||||||
CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson -> "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson -> "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
||||||
@@ -599,32 +598,7 @@ 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 (\(tbls, tblAccess, procs, schDesc) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache tbls tblAccess procs schDesc) body
|
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) 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,11 +27,10 @@ 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 (FieldName, QualifiedIdentifier (..))
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..), Junction (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..), Relationship (..),
|
||||||
Relationship (..), RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (FuncVolatility (..), Routine (..),
|
import PostgREST.SchemaCache.Routine (FuncVolatility (..), Routine (..),
|
||||||
RoutineParam (..))
|
RoutineParam (..))
|
||||||
import PostgREST.SchemaCache.Table (Column (..), Table (..), TablesMap,
|
import PostgREST.SchemaCache.Table (Column (..), Table (..), TablesMap,
|
||||||
@@ -39,27 +38,18 @@ import PostgREST.SchemaCache.Table (Column (..), Table (..), TablesMap,
|
|||||||
|
|
||||||
import Protolude hiding (Proxy, get)
|
import Protolude hiding (Proxy, get)
|
||||||
|
|
||||||
encode :: (Text, Text) -> AppConfig -> SchemaCache -> TablesMap -> TablesAccess -> HM.HashMap k [Routine] -> Maybe Text -> LBS.ByteString
|
encode :: (Text, Text) -> AppConfig -> SchemaCache -> TablesMap -> HM.HashMap k [Routine] -> Maybe Text -> LBS.ByteString
|
||||||
encode versions conf sCache tables access procs schemaDescription =
|
encode versions conf sCache tables procs schemaDescription =
|
||||||
JSON.encode $
|
JSON.encode $
|
||||||
postgrestSpec
|
postgrestSpec
|
||||||
versions
|
versions
|
||||||
(dbRelationships sCache)
|
(dbRelationships sCache)
|
||||||
(concat $ HM.elems procs)
|
(concat $ HM.elems procs)
|
||||||
(fmap (\(_, t) -> (t, accessFor access t)) (HM.toList tables))
|
(snd <$> 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
|
||||||
|
|
||||||
@@ -107,41 +97,14 @@ parseDefault colType colDefault =
|
|||||||
where
|
where
|
||||||
wrapInQuotations text = "\"" <> text <> "\""
|
wrapInQuotations text = "\"" <> text <> "\""
|
||||||
|
|
||||||
makeTableDef :: RelationshipsMap -> (Table, TableAccess) -> (Text, Schema)
|
makeTableDef :: RelationshipsMap -> Table -> (Text, Schema)
|
||||||
makeTableDef rels (t, access) =
|
makeTableDef rels t =
|
||||||
|
let tn = tableName t in
|
||||||
(tn, (mempty :: Schema)
|
(tn, (mempty :: Schema)
|
||||||
& description .~ tblDescription
|
& 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)
|
|
||||||
tblDescription = case m2mMarkers t rels of
|
|
||||||
[] -> tableDescription t
|
|
||||||
ms -> Just $ maybe "" (`T.append` "\n\n") (tableDescription t) <> T.intercalate "\n" ms
|
|
||||||
|
|
||||||
-- | Emits markers for the many-to-many relationships of a table, so that clients
|
|
||||||
-- can render these relations. The marker includes the target table(embedding key),
|
|
||||||
-- the junction table and the junction columns referencing source and target.
|
|
||||||
m2mMarkers :: Table -> RelationshipsMap -> [Text]
|
|
||||||
m2mMarkers tbl rels = mapMaybe m2mMarker searchedRels
|
|
||||||
where
|
|
||||||
searchedRels = fromMaybe mempty $ HM.lookup (QualifiedIdentifier (tableSchema tbl) (tableName tbl), tableSchema tbl) rels
|
|
||||||
m2mMarker Relationship{relForeignTable, relCardinality=M2M junction} =
|
|
||||||
Just $ T.intercalate ""
|
|
||||||
[ "<m2m table='", qiName relForeignTable
|
|
||||||
, "' junction='", qiName (junTable junction)
|
|
||||||
, "' source='", junctionSourceCol junction
|
|
||||||
, "' target='", junctionTargetCol junction
|
|
||||||
, "'/>"
|
|
||||||
]
|
|
||||||
m2mMarker _ = Nothing
|
|
||||||
junctionSourceCol junction = maybe mempty snd (headMay $ junColsSource junction)
|
|
||||||
junctionTargetCol junction = maybe mempty snd (headMay $ junColsTarget junction)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -166,18 +129,11 @@ makeProperty tbl rels col = (colName col, Inline s)
|
|||||||
(\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]) <$> fTblCol
|
(\(a, b) -> T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]) <$> fTblCol
|
||||||
pk :: Bool
|
pk :: Bool
|
||||||
pk = colName col `elem` tablePKCols tbl
|
pk = colName col `elem` tablePKCols tbl
|
||||||
uniqueNotes :: [Text]
|
|
||||||
uniqueNotes = mapMaybe uniqueNote (filter (colName col `elem`) (tableUniqueCols tbl))
|
|
||||||
where
|
|
||||||
uniqueNote cols
|
|
||||||
| length cols == 1 = Just "This is a Unique column.<unique/>"
|
|
||||||
| otherwise = Just $ "This is part of a composite unique constraint.<unique cols='" <> T.intercalate "," cols <> "'/>"
|
|
||||||
n = catMaybes
|
n = catMaybes
|
||||||
[ Just "Note:"
|
[ Just "Note:"
|
||||||
, if pk then Just "This is a Primary Key.<pk/>" else Nothing
|
, if pk then Just "This is a Primary Key.<pk/>" else Nothing
|
||||||
|
, fk
|
||||||
]
|
]
|
||||||
<> uniqueNotes
|
|
||||||
<> catMaybes [fk]
|
|
||||||
d =
|
d =
|
||||||
if length n > 1 then
|
if length n > 1 then
|
||||||
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
||||||
@@ -266,8 +222,8 @@ makeProcPostParams pd =
|
|||||||
, Ref $ Reference "preferParams"
|
, Ref $ Reference "preferParams"
|
||||||
]
|
]
|
||||||
|
|
||||||
makeParamDefs :: RelationshipsMap -> [(Table, TableAccess)] -> [(Text, Param)]
|
makeParamDefs :: [Table] -> [(Text, Param)]
|
||||||
makeParamDefs rels tis =
|
makeParamDefs ti =
|
||||||
-- 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"])
|
||||||
@@ -324,27 +280,17 @@ makeParamDefs rels tis =
|
|||||||
& in_ .~ ParamQuery
|
& in_ .~ ParamQuery
|
||||||
& type_ ?~ SwaggerString))
|
& type_ ?~ SwaggerString))
|
||||||
]
|
]
|
||||||
<> concat [ makeObjectBody rels t access <> makeRowFilters (tableName t) (accessibleCols t (taSelectCols access))
|
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) (tableColumnsList t)
|
||||||
| (t, access) <- tis
|
| t <- ti
|
||||||
]
|
]
|
||||||
|
|
||||||
makeObjectBody :: RelationshipsMap -> Table -> TableAccess -> [(Text, Param)]
|
makeObjectBody :: Text -> (Text, Param)
|
||||||
makeObjectBody rels t access =
|
makeObjectBody tn =
|
||||||
[ ("body." <> tn, makeBodyParam (taInsertCols access))
|
("body." <> tn, (mempty :: Param)
|
||||||
, ("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 (Inline bodySchema)
|
& schema .~ ParamBody (Ref (Reference tn)))
|
||||||
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 =
|
||||||
@@ -359,8 +305,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, TableAccess) -> (FilePath, PathItem)
|
makePathItem :: Table -> (FilePath, PathItem)
|
||||||
makePathItem (t, access) = ("/" ++ T.unpack tn, p)
|
makePathItem t = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)
|
||||||
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
|
||||||
@@ -381,26 +327,20 @@ makePathItem (t, access) = ("/" ++ T.unpack tn, p)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
postOp = tOp
|
postOp = tOp
|
||||||
& parameters .~ fmap ref [bodyParam, "select", "preferPost"]
|
& parameters .~ fmap ref ["body." <> tn, "select", "preferPost"]
|
||||||
& at 201 ?~ "Created"
|
& at 201 ?~ "Created"
|
||||||
patchOp = tOp
|
patchOp = tOp
|
||||||
& parameters .~ fmap ref (rs <> [patchBodyParam, "preferReturn"])
|
& parameters .~ fmap ref (rs <> ["body." <> tn, "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"
|
||||||
p = (mempty :: PathItem)
|
pr = (mempty :: PathItem) & get ?~ getOp
|
||||||
& get .~ (if not (null selCols) then Just getOp else Nothing)
|
pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp
|
||||||
& post .~ (if tableInsertable t && not (null insCols) then Just postOp else Nothing)
|
p False = pr
|
||||||
& patch .~ (if tableUpdatable t && not (null updCols) then Just patchOp else Nothing)
|
p True = pw
|
||||||
& delete .~ (if tableDeletable t && taDelete access then Just deletOp else Nothing)
|
|
||||||
tn = tableName t
|
tn = tableName t
|
||||||
selCols = accessibleCols t (taSelectCols access)
|
rs = [ T.intercalate "." ["rowFilter", tn, colName c ] | c <- tableColumnsList t ]
|
||||||
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)
|
||||||
@@ -435,9 +375,9 @@ makeRootPathItem = ("/", p)
|
|||||||
pr = (mempty :: PathItem) & get ?~ getOp
|
pr = (mempty :: PathItem) & get ?~ getOp
|
||||||
p = pr
|
p = pr
|
||||||
|
|
||||||
makePathItems :: [Routine] -> [(Table, TableAccess)] -> InsOrdHashMap FilePath PathItem
|
makePathItems :: [Routine] -> [Table] -> InsOrdHashMap FilePath PathItem
|
||||||
makePathItems pds tis = fromList $ makeRootPathItem :
|
makePathItems pds ti = fromList $ makeRootPathItem :
|
||||||
fmap makePathItem tis ++ fmap makeProcPathItem pds
|
fmap makePathItem ti ++ fmap makeProcPathItem pds
|
||||||
|
|
||||||
makeSecurityDefinitions :: Text -> Bool -> SecurityDefinitions
|
makeSecurityDefinitions :: Text -> Bool -> SecurityDefinitions
|
||||||
makeSecurityDefinitions secName allow
|
makeSecurityDefinitions secName allow
|
||||||
@@ -447,8 +387,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, TableAccess)] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger
|
postgrestSpec :: (Text, Text) -> RelationshipsMap -> [Routine] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger
|
||||||
postgrestSpec (prettyVersion, docsVersion) rels pds tis (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)
|
postgrestSpec (prettyVersion, docsVersion) rels pds ti (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)
|
||||||
@@ -459,9 +399,9 @@ postgrestSpec (prettyVersion, docsVersion) rels pds tis (s, h, p, b) sd allowSec
|
|||||||
& 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 <$> tis)
|
& definitions .~ fromList (makeTableDef rels <$> ti)
|
||||||
& parameters .~ fromList (makeParamDefs rels tis)
|
& parameters .~ fromList (makeParamDefs ti)
|
||||||
& paths .~ makePathItems pds tis
|
& paths .~ makePathItems pds ti
|
||||||
& 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
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ querySchemaCache pgVer conf@AppConfig{..} = do
|
|||||||
m2oRels <- sqlTimedStmt gucRels mempty allM2OandO2ORels
|
m2oRels <- sqlTimedStmt gucRels mempty allM2OandO2ORels
|
||||||
funcs <- sqlTimedStmt gucFuncs conf (allFunctions pgVer configDbPreparedStatements)
|
funcs <- sqlTimedStmt gucFuncs conf (allFunctions pgVer configDbPreparedStatements)
|
||||||
cRels <- sqlTimedStmt gucCRels mempty allComputedRels
|
cRels <- sqlTimedStmt gucCRels mempty allComputedRels
|
||||||
rlsPols <- sqlTimedStmt gucRLS conf allRlsPolicies
|
|
||||||
reps <- sqlTimedStmt gucDReps conf dataRepresentations
|
reps <- sqlTimedStmt gucDReps conf dataRepresentations
|
||||||
mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers
|
mHdlers <- sqlTimedStmt gucMHdrs conf mediaHandlers
|
||||||
|
|
||||||
@@ -162,11 +161,10 @@ querySchemaCache pgVer conf@AppConfig{..} = do
|
|||||||
else pure Nothing
|
else pure Nothing
|
||||||
|
|
||||||
let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
|
let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
|
||||||
tabsWithRls = addRlsQuals tabsWViewsPks (combineRlsPolicies rlsPols)
|
|
||||||
rels = addInverseRels $ addM2MRels tabsWViewsPks $ addViewM2OAndO2ORels keyDeps m2oRels
|
rels = addInverseRels $ addM2MRels tabsWViewsPks $ addViewM2OAndO2ORels keyDeps m2oRels
|
||||||
|
|
||||||
return (removeInternal schemas $ SchemaCache {
|
return (removeInternal schemas $ SchemaCache {
|
||||||
dbTables = tabsWithRls
|
dbTables = tabsWViewsPks
|
||||||
, dbRelationships = getOverrideRelationshipsMap rels cRels
|
, dbRelationships = getOverrideRelationshipsMap rels cRels
|
||||||
, dbRoutines = funcs
|
, dbRoutines = funcs
|
||||||
, dbRepresentations = reps
|
, dbRepresentations = reps
|
||||||
@@ -234,7 +232,6 @@ decodeTables =
|
|||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> arrayColumn HD.text
|
<*> arrayColumn HD.text
|
||||||
<*> column (HD.refine parseUniqueCols HD.jsonb)
|
|
||||||
<*> parseCols (compositeArrayColumn
|
<*> parseCols (compositeArrayColumn
|
||||||
(Column
|
(Column
|
||||||
<$> compositeField HD.text
|
<$> compositeField HD.text
|
||||||
@@ -245,19 +242,11 @@ decodeTables =
|
|||||||
<*> nullableCompositeField HD.int4
|
<*> nullableCompositeField HD.int4
|
||||||
<*> nullableCompositeField HD.text
|
<*> nullableCompositeField HD.text
|
||||||
<*> compositeFieldArray HD.text))
|
<*> compositeFieldArray HD.text))
|
||||||
<*> pure (Nothing :: Maybe Text)
|
|
||||||
<*> pure (Nothing :: Maybe Text)
|
|
||||||
|
|
||||||
|
|
||||||
parseCols :: HD.Row [Column] -> HD.Row ColumnMap
|
parseCols :: HD.Row [Column] -> HD.Row ColumnMap
|
||||||
parseCols = fmap (HMI.fromList . map (\col@Column{colName} -> (colName, col)))
|
parseCols = fmap (HMI.fromList . map (\col@Column{colName} -> (colName, col)))
|
||||||
|
|
||||||
parseUniqueCols :: JSON.Value -> Either Text [[FieldName]]
|
|
||||||
parseUniqueCols val =
|
|
||||||
case JSON.fromJSON val of
|
|
||||||
JSON.Success cols -> Right cols
|
|
||||||
JSON.Error err -> Left ("Invalid unique columns: " <> T.pack err)
|
|
||||||
|
|
||||||
decodeRels :: HD.Result [Relationship]
|
decodeRels :: HD.Result [Relationship]
|
||||||
decodeRels =
|
decodeRels =
|
||||||
HD.rowList relRow
|
HD.rowList relRow
|
||||||
@@ -687,25 +676,6 @@ tablesSqlQuery pgVer =
|
|||||||
AND NOT pg_is_other_temp_schema(r.relnamespace)
|
AND NOT pg_is_other_temp_schema(r.relnamespace)
|
||||||
AND NOT a.attisdropped
|
AND NOT a.attisdropped
|
||||||
GROUP BY r.oid
|
GROUP BY r.oid
|
||||||
),
|
|
||||||
tbl_unique_cols AS (
|
|
||||||
SELECT
|
|
||||||
r.oid AS relid,
|
|
||||||
jsonb_agg(cols ORDER BY c.oid) AS unique_cols
|
|
||||||
FROM pg_class r
|
|
||||||
JOIN pg_constraint c
|
|
||||||
ON r.oid = c.conrelid
|
|
||||||
JOIN LATERAL (
|
|
||||||
SELECT jsonb_agg(a.attname::text ORDER BY k.ord) AS cols
|
|
||||||
FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
|
|
||||||
JOIN pg_attribute a ON a.attrelid = r.oid AND a.attnum = k.attnum
|
|
||||||
) col_info ON TRUE
|
|
||||||
WHERE
|
|
||||||
c.contype = 'u'
|
|
||||||
AND r.relkind IN ('r', 'p')
|
|
||||||
AND r.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace)
|
|
||||||
AND NOT pg_is_other_temp_schema(r.relnamespace)
|
|
||||||
GROUP BY r.oid
|
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
n.nspname AS table_schema,
|
n.nspname AS table_schema,
|
||||||
@@ -739,13 +709,11 @@ tablesSqlQuery pgVer =
|
|||||||
)
|
)
|
||||||
) AS deletable,
|
) AS deletable,
|
||||||
coalesce(tpks.pk_cols, '{}') as pk_cols,
|
coalesce(tpks.pk_cols, '{}') as pk_cols,
|
||||||
coalesce(tunq.unique_cols, '[]'::jsonb) as unique_cols,
|
|
||||||
coalesce(cols_agg.columns, '{}') as columns
|
coalesce(cols_agg.columns, '{}') as columns
|
||||||
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
|
||||||
LEFT JOIN pg_description d on d.objoid = c.oid and d.objsubid = 0 and d.classoid = 'pg_class'::regclass
|
LEFT JOIN pg_description d on d.objoid = c.oid and d.objsubid = 0 and d.classoid = 'pg_class'::regclass
|
||||||
LEFT JOIN tbl_pk_cols tpks ON c.oid = tpks.relid
|
LEFT JOIN tbl_pk_cols tpks ON c.oid = tpks.relid
|
||||||
LEFT JOIN tbl_unique_cols tunq ON c.oid = tunq.relid
|
|
||||||
LEFT JOIN columns_agg cols_agg ON c.oid = cols_agg.relid
|
LEFT JOIN columns_agg cols_agg ON c.oid = cols_agg.relid
|
||||||
WHERE c.relkind IN ('v','r','m','f','p')
|
WHERE c.relkind IN ('v','r','m','f','p')
|
||||||
AND c.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace)
|
AND c.relnamespace NOT IN ('pg_catalog'::regnamespace, 'information_schema'::regnamespace)
|
||||||
@@ -842,88 +810,6 @@ allComputedRels =
|
|||||||
column HD.bool <*>
|
column HD.bool <*>
|
||||||
column HD.bool
|
column HD.bool
|
||||||
|
|
||||||
-- | A row-level security policy of an exposed table, gathered from pg_policies.
|
|
||||||
data RlsPolicyRow = RlsPolicyRow
|
|
||||||
{ rlsTable :: QualifiedIdentifier
|
|
||||||
, rlsRowSec :: Bool
|
|
||||||
, rlsCmd :: Text -- ^ "w" (UPDATE), "d" (DELETE), "*" (ALL)
|
|
||||||
, rlsPermiss :: Bool
|
|
||||||
, rlsQual :: Maybe Text -- ^ USING qualifier; Nothing means the policy has no USING restriction
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Returns the UPDATE/DELETE RLS policies of the exposed tables, so that the
|
|
||||||
-- planner can surface per-row can_edit/can_delete fields on SELECT *.
|
|
||||||
allRlsPolicies :: SQL.Statement AppConfig [RlsPolicyRow]
|
|
||||||
allRlsPolicies =
|
|
||||||
SQL.Statement sql params decodeRlsPolicies True
|
|
||||||
where
|
|
||||||
params = map escapeIdent . toList . configDbSchemas >$< arrayParam HE.text
|
|
||||||
sql = encodeUtf8 [trimming|
|
|
||||||
SELECT
|
|
||||||
n.nspname::text AS table_schema,
|
|
||||||
c.relname::text AS table_name,
|
|
||||||
c.relrowsecurity AS row_security,
|
|
||||||
p.polcmd::text AS cmd,
|
|
||||||
p.polpermissive AS permissive,
|
|
||||||
pg_get_expr(p.polqual, p.polrelid) AS qual
|
|
||||||
FROM pg_class c
|
|
||||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
||||||
JOIN pg_policy p ON p.polrelid = c.oid
|
|
||||||
WHERE c.relkind IN ('r','p')
|
|
||||||
AND c.relnamespace = ANY($$1::regnamespace[])
|
|
||||||
AND p.polcmd IN ('w','d','*')
|
|
||||||
ORDER BY n.nspname, c.relname|]
|
|
||||||
|
|
||||||
decodeRlsPolicies :: HD.Result [RlsPolicyRow]
|
|
||||||
decodeRlsPolicies =
|
|
||||||
HD.rowList rlsRow
|
|
||||||
where
|
|
||||||
rlsRow = RlsPolicyRow
|
|
||||||
<$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
|
|
||||||
<*> column HD.bool
|
|
||||||
<*> column HD.text
|
|
||||||
<*> column HD.bool
|
|
||||||
<*> nullableColumn HD.text
|
|
||||||
|
|
||||||
-- | Combines the per-command RLS policies of each table into the SQL expression
|
|
||||||
-- used to compute can_edit/can_delete. Following PostgreSQL semantics, multiple
|
|
||||||
-- permissive policies combine with OR and restrictive ones with AND.
|
|
||||||
combineRlsPolicies :: [RlsPolicyRow] -> HM.HashMap QualifiedIdentifier (Maybe Text, Maybe Text)
|
|
||||||
combineRlsPolicies rows = HM.fromList $ mapMaybe toEntry $ HM.toList byTable
|
|
||||||
where
|
|
||||||
byTable = HM.fromListWith (<>) [ (rlsTable r, [r]) | r <- rows ]
|
|
||||||
|
|
||||||
toEntry (qi, rs)
|
|
||||||
| not (any rlsRowSec rs) = Nothing
|
|
||||||
| isNothing editQ && isNothing delQ = Nothing
|
|
||||||
| otherwise = Just (qi, (editQ, delQ))
|
|
||||||
where
|
|
||||||
editQ = combineCmd ["w", "*"] rs
|
|
||||||
delQ = combineCmd ["d", "*"] rs
|
|
||||||
|
|
||||||
combineCmd cmds rs
|
|
||||||
| null policies = Nothing
|
|
||||||
| null permissiveQs = Nothing
|
|
||||||
| otherwise = Just $ wrapQual combined
|
|
||||||
where
|
|
||||||
policies = [ r | r <- rs, rlsCmd r `elem` cmds ]
|
|
||||||
permissiveQs = [ fromMaybe "true" (rlsQual r) | r <- policies, rlsPermiss r ]
|
|
||||||
restrictiveQs = [ fromMaybe "true" (rlsQual r) | r <- policies, not (rlsPermiss r) ]
|
|
||||||
permissiveExpr = T.intercalate " OR " permissiveQs
|
|
||||||
combined = case restrictiveQs of
|
|
||||||
[] -> permissiveExpr
|
|
||||||
_ -> "(" <> permissiveExpr <> ") AND (" <> T.intercalate " AND " restrictiveQs <> ")"
|
|
||||||
|
|
||||||
wrapQual q = "COALESCE(" <> q <> ", false)"
|
|
||||||
|
|
||||||
-- | Attaches the combined RLS qualifiers to the corresponding tables.
|
|
||||||
addRlsQuals :: TablesMap -> HM.HashMap QualifiedIdentifier (Maybe Text, Maybe Text) -> TablesMap
|
|
||||||
addRlsQuals tabs rlsMap = HM.mapWithKey setRls tabs
|
|
||||||
where
|
|
||||||
setRls qi tbl = case HM.lookup qi rlsMap of
|
|
||||||
Nothing -> tbl
|
|
||||||
Just (editQ, delQ) -> tbl { tableRlsEditQual = editQ, tableRlsDeleteQual = delQ }
|
|
||||||
|
|
||||||
-- | Returns all the views' primary keys and foreign keys dependencies
|
-- | Returns all the views' primary keys and foreign keys dependencies
|
||||||
allViewsKeyDependencies :: SQL.Statement AppConfig [ViewKeyDependency]
|
allViewsKeyDependencies :: SQL.Statement AppConfig [ViewKeyDependency]
|
||||||
allViewsKeyDependencies =
|
allViewsKeyDependencies =
|
||||||
@@ -1264,15 +1150,15 @@ extractTimings = SQL.Statement sql HE.noParams decodeThem True
|
|||||||
qFrag setting = "extract('milliseconds' from current_setting('pgrst." <> setting <> "', false)::interval)::text"
|
qFrag setting = "extract('milliseconds' from current_setting('pgrst." <> setting <> "', false)::interval)::text"
|
||||||
sql = "SELECT " <> BS.intercalate ","
|
sql = "SELECT " <> BS.intercalate ","
|
||||||
[ qFrag gucTbls, qFrag gucKDeps, qFrag gucRels
|
[ qFrag gucTbls, qFrag gucKDeps, qFrag gucRels
|
||||||
, qFrag gucFuncs, qFrag gucCRels, qFrag gucRLS
|
, qFrag gucFuncs, qFrag gucCRels, qFrag gucDReps
|
||||||
, qFrag gucDReps, qFrag gucMHdrs
|
, qFrag gucMHdrs
|
||||||
]
|
]
|
||||||
decodeThem :: HD.Result QueryTimings
|
decodeThem :: HD.Result QueryTimings
|
||||||
decodeThem = HD.singleRow $
|
decodeThem = HD.singleRow $
|
||||||
QueryTimings
|
QueryTimings
|
||||||
<$> column HD.text <*> column HD.text <*> column HD.text
|
<$> column HD.text <*> column HD.text <*> column HD.text
|
||||||
<*> column HD.text <*> column HD.text <*> column HD.text
|
<*> column HD.text <*> column HD.text <*> column HD.text
|
||||||
<*> column HD.text <*> column HD.text
|
<*> column HD.text
|
||||||
|
|
||||||
data QueryTimings = QueryTimings
|
data QueryTimings = QueryTimings
|
||||||
{ qtTables :: Text
|
{ qtTables :: Text
|
||||||
@@ -1280,7 +1166,6 @@ data QueryTimings = QueryTimings
|
|||||||
, qtRels :: Text
|
, qtRels :: Text
|
||||||
, qtFuncs :: Text
|
, qtFuncs :: Text
|
||||||
, qtCRels :: Text
|
, qtCRels :: Text
|
||||||
, qtRls :: Text
|
|
||||||
, qtDReps :: Text
|
, qtDReps :: Text
|
||||||
, qtMHdrs :: Text
|
, qtMHdrs :: Text
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
@@ -1292,17 +1177,15 @@ queryTimingsWLabels qt =
|
|||||||
, (gucRels, qtRels qt)
|
, (gucRels, qtRels qt)
|
||||||
, (gucFuncs, qtFuncs qt)
|
, (gucFuncs, qtFuncs qt)
|
||||||
, (gucCRels, qtCRels qt)
|
, (gucCRels, qtCRels qt)
|
||||||
, (gucRLS, qtRls qt)
|
|
||||||
, (gucDReps, qtDReps qt)
|
, (gucDReps, qtDReps qt)
|
||||||
, (gucMHdrs, qtMHdrs qt)
|
, (gucMHdrs, qtMHdrs qt)
|
||||||
]
|
]
|
||||||
|
|
||||||
gucTbls, gucKDeps, gucRels, gucFuncs, gucCRels, gucRLS, gucDReps, gucMHdrs :: ByteString
|
gucTbls, gucKDeps, gucRels, gucFuncs, gucCRels, gucDReps, gucMHdrs :: ByteString
|
||||||
gucTbls = "tables"
|
gucTbls = "tables"
|
||||||
gucKDeps = "keydeps"
|
gucKDeps = "keydeps"
|
||||||
gucRels = "rels"
|
gucRels = "rels"
|
||||||
gucFuncs = "funcs"
|
gucFuncs = "funcs"
|
||||||
gucCRels = "comprels"
|
gucCRels = "comprels"
|
||||||
gucRLS = "rls"
|
|
||||||
gucDReps = "dreps"
|
gucDReps = "dreps"
|
||||||
gucMHdrs = "mhandlers"
|
gucMHdrs = "mhandlers"
|
||||||
|
|||||||
@@ -29,18 +29,7 @@ data Table = Table
|
|||||||
, tableUpdatable :: Bool
|
, tableUpdatable :: Bool
|
||||||
, tableDeletable :: Bool
|
, tableDeletable :: Bool
|
||||||
, tablePKCols :: [FieldName]
|
, tablePKCols :: [FieldName]
|
||||||
-- ^ Each element is the position-ordered column list of a unique
|
|
||||||
-- constraint. A single-column unique constraint is represented by a
|
|
||||||
-- single-element list.
|
|
||||||
, tableUniqueCols :: [[FieldName]]
|
|
||||||
, tableColumns :: ColumnMap
|
, tableColumns :: ColumnMap
|
||||||
, tableRlsEditQual :: Maybe Text
|
|
||||||
-- ^ Combined RLS UPDATE USING qualifier, COALESCE-wrapped. Nothing means
|
|
||||||
-- the table has no row-level edit restriction to surface (RLS disabled or
|
|
||||||
-- no applicable UPDATE policy).
|
|
||||||
, tableRlsDeleteQual :: Maybe Text
|
|
||||||
-- ^ Combined RLS DELETE USING qualifier, COALESCE-wrapped. Nothing means
|
|
||||||
-- the table has no row-level delete restriction to surface.
|
|
||||||
}
|
}
|
||||||
deriving (Show, Generic, JSON.ToJSON)
|
deriving (Show, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
|||||||
@@ -7,44 +7,7 @@
|
|||||||
tableIsView: false
|
tableIsView: false
|
||||||
tableName: authors_only
|
tableName: authors_only
|
||||||
tablePKCols: []
|
tablePKCols: []
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
|
||||||
|
|
||||||
- - qiName: no_rls_items
|
|
||||||
qiSchema: public
|
|
||||||
- tableColumns:
|
|
||||||
id:
|
|
||||||
colDefault: null
|
|
||||||
colDescription: null
|
|
||||||
colEnum: []
|
|
||||||
colMaxLen: null
|
|
||||||
colName: id
|
|
||||||
colNominalType: integer
|
|
||||||
colNullable: false
|
|
||||||
colType: integer
|
|
||||||
name:
|
|
||||||
colDefault: null
|
|
||||||
colDescription: null
|
|
||||||
colEnum: []
|
|
||||||
colMaxLen: null
|
|
||||||
colName: name
|
|
||||||
colNominalType: text
|
|
||||||
colNullable: true
|
|
||||||
colType: text
|
|
||||||
tableDeletable: true
|
|
||||||
tableDescription: null
|
|
||||||
tableInsertable: true
|
|
||||||
tableIsView: false
|
|
||||||
tableName: no_rls_items
|
|
||||||
tablePKCols:
|
|
||||||
- id
|
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|
||||||
- - qiName: cats
|
- - qiName: cats
|
||||||
@@ -75,55 +38,7 @@
|
|||||||
tableName: cats
|
tableName: cats
|
||||||
tablePKCols:
|
tablePKCols:
|
||||||
- id
|
- id
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
|
||||||
|
|
||||||
- - qiName: rls_items
|
|
||||||
qiSchema: public
|
|
||||||
- tableColumns:
|
|
||||||
account_id:
|
|
||||||
colDefault: null
|
|
||||||
colDescription: null
|
|
||||||
colEnum: []
|
|
||||||
colMaxLen: null
|
|
||||||
colName: account_id
|
|
||||||
colNominalType: bigint
|
|
||||||
colNullable: true
|
|
||||||
colType: bigint
|
|
||||||
id:
|
|
||||||
colDefault: null
|
|
||||||
colDescription: null
|
|
||||||
colEnum: []
|
|
||||||
colMaxLen: null
|
|
||||||
colName: id
|
|
||||||
colNominalType: integer
|
|
||||||
colNullable: false
|
|
||||||
colType: integer
|
|
||||||
name:
|
|
||||||
colDefault: null
|
|
||||||
colDescription: null
|
|
||||||
colEnum: []
|
|
||||||
colMaxLen: null
|
|
||||||
colName: name
|
|
||||||
colNominalType: text
|
|
||||||
colNullable: true
|
|
||||||
colType: text
|
|
||||||
tableDeletable: true
|
|
||||||
tableDescription: null
|
|
||||||
tableInsertable: true
|
|
||||||
tableIsView: false
|
|
||||||
tableName: rls_items
|
|
||||||
tablePKCols:
|
|
||||||
- id
|
|
||||||
tableRlsDeleteQual: COALESCE(((((current_setting('request.jwt.claims'::text, true))::json
|
|
||||||
->> 'account_id'::text))::bigint = account_id), false)
|
|
||||||
tableRlsEditQual: COALESCE(((((current_setting('request.jwt.claims'::text, true))::json
|
|
||||||
->> 'account_id'::text))::bigint = account_id), false)
|
|
||||||
tableSchema: public
|
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|
||||||
- - qiName: items_w_isolation_level
|
- - qiName: items_w_isolation_level
|
||||||
@@ -153,10 +68,7 @@
|
|||||||
tableIsView: true
|
tableIsView: true
|
||||||
tableName: items_w_isolation_level
|
tableName: items_w_isolation_level
|
||||||
tablePKCols: []
|
tablePKCols: []
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|
||||||
- - qiName: directors
|
- - qiName: directors
|
||||||
@@ -187,10 +99,7 @@
|
|||||||
tableName: directors
|
tableName: directors
|
||||||
tablePKCols:
|
tablePKCols:
|
||||||
- id
|
- id
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|
||||||
- - qiName: projects
|
- - qiName: projects
|
||||||
@@ -202,10 +111,7 @@
|
|||||||
tableIsView: false
|
tableIsView: false
|
||||||
tableName: projects
|
tableName: projects
|
||||||
tablePKCols: []
|
tablePKCols: []
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|
||||||
- - qiName: infinite_recursion
|
- - qiName: infinite_recursion
|
||||||
@@ -217,10 +123,7 @@
|
|||||||
tableIsView: true
|
tableIsView: true
|
||||||
tableName: infinite_recursion
|
tableName: infinite_recursion
|
||||||
tablePKCols: []
|
tablePKCols: []
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: false
|
tableUpdatable: false
|
||||||
|
|
||||||
- - qiName: awards
|
- - qiName: awards
|
||||||
@@ -278,10 +181,7 @@
|
|||||||
tableName: awards
|
tableName: awards
|
||||||
tablePKCols:
|
tablePKCols:
|
||||||
- id
|
- id
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|
||||||
- - qiName: films
|
- - qiName: films
|
||||||
@@ -321,10 +221,7 @@
|
|||||||
tableName: films
|
tableName: films
|
||||||
tablePKCols:
|
tablePKCols:
|
||||||
- id
|
- id
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|
||||||
- - qiName: items
|
- - qiName: items
|
||||||
@@ -345,8 +242,5 @@
|
|||||||
tableIsView: false
|
tableIsView: false
|
||||||
tableName: items
|
tableName: items
|
||||||
tablePKCols: []
|
tablePKCols: []
|
||||||
tableRlsDeleteQual: null
|
|
||||||
tableRlsEditQual: null
|
|
||||||
tableSchema: public
|
tableSchema: public
|
||||||
tableUniqueCols: []
|
|
||||||
tableUpdatable: true
|
tableUpdatable: true
|
||||||
|
|||||||
@@ -7,8 +7,3 @@ GRANT SELECT ON directors, films, awards TO postgrest_test_anonymous, postgrest_
|
|||||||
|
|
||||||
GRANT ALL ON cats TO postgrest_test_anonymous;
|
GRANT ALL ON cats TO postgrest_test_anonymous;
|
||||||
GRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
|
GRANT ALL ON items_w_isolation_level TO postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
|
||||||
|
|
||||||
GRANT SELECT ON rls_items TO postgrest_test_author;
|
|
||||||
GRANT UPDATE(name) ON rls_items TO postgrest_test_author;
|
|
||||||
GRANT DELETE ON rls_items TO postgrest_test_author;
|
|
||||||
GRANT SELECT ON no_rls_items TO postgrest_test_author;
|
|
||||||
|
|||||||
@@ -268,34 +268,3 @@ $$ language sql;
|
|||||||
create or replace function get_work_mem() returns text as $$
|
create or replace function get_work_mem() returns text as $$
|
||||||
select current_setting('work_mem', true);
|
select current_setting('work_mem', true);
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
-- RLS fixtures for testing can_edit/can_delete computed fields
|
|
||||||
create table rls_items(
|
|
||||||
id int primary key,
|
|
||||||
account_id bigint,
|
|
||||||
name text
|
|
||||||
);
|
|
||||||
|
|
||||||
alter table rls_items enable row level security;
|
|
||||||
|
|
||||||
create policy rls_items_select on rls_items for select
|
|
||||||
using (
|
|
||||||
account_id is null
|
|
||||||
or (current_setting('request.jwt.claims', true)::json ->> 'account_id')::bigint = account_id
|
|
||||||
);
|
|
||||||
|
|
||||||
create policy rls_items_update on rls_items for update
|
|
||||||
using ((current_setting('request.jwt.claims', true)::json ->> 'account_id')::bigint = account_id);
|
|
||||||
|
|
||||||
create policy rls_items_delete on rls_items for delete
|
|
||||||
using ((current_setting('request.jwt.claims', true)::json ->> 'account_id')::bigint = account_id);
|
|
||||||
|
|
||||||
insert into rls_items(id, account_id, name) values (1, 1, 'own'), (2, null, 'public'), (3, 2, 'other');
|
|
||||||
|
|
||||||
-- no RLS at all: can_edit/can_delete must be omitted
|
|
||||||
create table no_rls_items(
|
|
||||||
id int primary key,
|
|
||||||
name text
|
|
||||||
);
|
|
||||||
|
|
||||||
insert into no_rls_items(id, name) values (1, 'a'), (2, 'b');
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ def test_pool_acquisition_timeout(level, defaultenv, metapostgrest):
|
|||||||
assert data["message"] == "Timed out acquiring connection from connection pool."
|
assert data["message"] == "Timed out acquiring connection from connection pool."
|
||||||
|
|
||||||
# ensure the message appears on the logs as well
|
# ensure the message appears on the logs as well
|
||||||
output = sorted(drain_stdout(postgrest))
|
output = sorted(postgrest.read_stdout(nlines=10))
|
||||||
|
|
||||||
if level == "crit":
|
if level == "crit":
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
|
|||||||
+4
-4
@@ -44,7 +44,7 @@ def test_log_level(level, defaultenv):
|
|||||||
response = postgrest.session.get("/")
|
response = postgrest.session.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
output = drain_stdout(postgrest)
|
output = postgrest.read_stdout(nlines=9)
|
||||||
|
|
||||||
if level == "crit":
|
if level == "crit":
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
@@ -82,7 +82,7 @@ def test_log_level(level, defaultenv):
|
|||||||
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
assert len(output) > 3
|
assert len(output) == 9
|
||||||
assert any("Connection" and "is available" in line for line in output)
|
assert any("Connection" and "is available" in line for line in output)
|
||||||
assert any("Connection" and "is used" in line for line in output)
|
assert any("Connection" and "is used" in line for line in output)
|
||||||
|
|
||||||
@@ -403,7 +403,7 @@ def test_db_error_logging_to_stderr(level, defaultenv, metapostgrest):
|
|||||||
assert response.status_code == 500
|
assert response.status_code == 500
|
||||||
|
|
||||||
# ensure the message appears on the logs
|
# ensure the message appears on the logs
|
||||||
output = drain_stdout(postgrest)
|
output = postgrest.read_stdout(nlines=8)
|
||||||
|
|
||||||
if level == "crit":
|
if level == "crit":
|
||||||
assert len(output) == 0
|
assert len(output) == 0
|
||||||
@@ -456,7 +456,7 @@ def test_schema_cache_query_timings_log(level, defaultenv):
|
|||||||
"PGRST_LOG_LEVEL": level,
|
"PGRST_LOG_LEVEL": level,
|
||||||
}
|
}
|
||||||
log_pattern = re.compile(
|
log_pattern = re.compile(
|
||||||
r".+: tables: [\d.]+ ms, keydeps: [\d.]+ ms, rels: [\d.]+ ms, funcs: [\d.]+ ms, comprels: [\d.]+ ms, rls: [\d.]+ ms, dreps: [\d.]+ ms, mhandlers: [\d.]+ ms"
|
r".+: tables: [\d.]+ ms, keydeps: [\d.]+ ms, rels: [\d.]+ ms, funcs: [\d.]+ ms, comprels: [\d.]+ ms, dreps: [\d.]+ ms, mhandlers: [\d.]+ ms"
|
||||||
)
|
)
|
||||||
|
|
||||||
with run(env=env, no_startup_stdout=False) as postgrest:
|
with run(env=env, no_startup_stdout=False) as postgrest:
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
from config import SECRET
|
|
||||||
from postgrest import run
|
|
||||||
from util import jwtauthheader
|
|
||||||
|
|
||||||
|
|
||||||
def author_headers(account_id):
|
|
||||||
"Authorization header for postgrest_test_author with the given account id."
|
|
||||||
return jwtauthheader(
|
|
||||||
{"role": "postgrest_test_author", "account_id": account_id}, SECRET
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_rls_can_edit_can_delete(defaultenv):
|
|
||||||
"select * on an RLS table exposes can_edit/can_delete computed from the policies"
|
|
||||||
|
|
||||||
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
|
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
|
||||||
response = postgrest.session.get("/rls_items", headers=author_headers(1))
|
|
||||||
assert response.status_code == 200
|
|
||||||
rows = {r["id"]: r for r in response.json()}
|
|
||||||
|
|
||||||
# rows visible to account_id=1: own row and the public row
|
|
||||||
assert set(rows) == {1, 2}
|
|
||||||
|
|
||||||
# the own row can be edited and deleted
|
|
||||||
assert rows[1]["can_edit"] is True
|
|
||||||
assert rows[1]["can_delete"] is True
|
|
||||||
|
|
||||||
# the public row is visible but not editable or deletable
|
|
||||||
assert rows[2]["can_edit"] is False
|
|
||||||
assert rows[2]["can_delete"] is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_rls_omits_can_edit_can_delete(defaultenv):
|
|
||||||
"select * on a table without RLS omits the computed columns"
|
|
||||||
|
|
||||||
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
|
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
|
||||||
response = postgrest.session.get("/no_rls_items", headers=author_headers(1))
|
|
||||||
assert response.status_code == 200
|
|
||||||
rows = response.json()
|
|
||||||
|
|
||||||
assert len(rows) == 2
|
|
||||||
for row in rows:
|
|
||||||
assert "can_edit" not in row
|
|
||||||
assert "can_delete" not in row
|
|
||||||
|
|
||||||
|
|
||||||
def test_rls_columns_not_in_openapi(defaultenv):
|
|
||||||
"The OpenAPI spec must not advertise the computed columns"
|
|
||||||
|
|
||||||
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
|
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
|
||||||
response = postgrest.session.get("/", headers=author_headers(1))
|
|
||||||
assert response.status_code == 200
|
|
||||||
spec = response.json()
|
|
||||||
|
|
||||||
properties = spec["definitions"]["rls_items"]["properties"]
|
|
||||||
assert "can_edit" not in properties
|
|
||||||
assert "can_delete" not in properties
|
|
||||||
@@ -222,58 +222,6 @@ 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 "/"
|
||||||
|
|
||||||
@@ -285,91 +233,10 @@ spec withConfig = withConfig baseCfg $ describe "OpenAPI" $ do
|
|||||||
{
|
{
|
||||||
"format": "int32",
|
"format": "int32",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Note:\nThis is a Unique column.<unique/>\nThis is a Foreign Key to `second.id`.<fk table='second' column='id'/>"
|
"description": "Note:\nThis is a Foreign Key to `second.id`.<fk table='second' column='id'/>"
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
it "includes a unique description for a column with a unique constraint" $ do
|
|
||||||
r <- simpleBody <$> get "/"
|
|
||||||
|
|
||||||
let uniqueKey = r ^? key "definitions" . key "single_unique" . key "properties" . key "unique_key"
|
|
||||||
|
|
||||||
liftIO $
|
|
||||||
uniqueKey `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
{
|
|
||||||
"format": "int32",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Note:\nThis is a Unique column.<unique/>"
|
|
||||||
}
|
|
||||||
|]
|
|
||||||
|
|
||||||
it "includes the column list of a composite unique constraint" $ do
|
|
||||||
r <- simpleBody <$> get "/"
|
|
||||||
|
|
||||||
let compoundKey1 = r ^? key "definitions" . key "compound_unique" . key "properties" . key "key1"
|
|
||||||
compoundKey2 = r ^? key "definitions" . key "compound_unique" . key "properties" . key "key2"
|
|
||||||
|
|
||||||
liftIO $ do
|
|
||||||
compoundKey1 `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
{
|
|
||||||
"format": "int32",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
|
||||||
}
|
|
||||||
|]
|
|
||||||
compoundKey2 `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
{
|
|
||||||
"format": "int32",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
|
||||||
}
|
|
||||||
|]
|
|
||||||
|
|
||||||
it "includes the column list for mixed single and composite unique constraints" $ do
|
|
||||||
r <- simpleBody <$> get "/"
|
|
||||||
|
|
||||||
let uniqueCol = r ^? key "definitions" . key "mixed_unique" . key "properties" . key "id"
|
|
||||||
compoundKey1 = r ^? key "definitions" . key "mixed_unique" . key "properties" . key "key1"
|
|
||||||
compoundKey2 = r ^? key "definitions" . key "mixed_unique" . key "properties" . key "key2"
|
|
||||||
|
|
||||||
liftIO $ do
|
|
||||||
uniqueCol `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
{
|
|
||||||
"format": "int32",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Note:\nThis is a Unique column.<unique/>"
|
|
||||||
}
|
|
||||||
|]
|
|
||||||
compoundKey1 `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
{
|
|
||||||
"format": "int32",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
|
||||||
}
|
|
||||||
|]
|
|
||||||
compoundKey2 `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
{
|
|
||||||
"format": "int32",
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Note:\nThis is part of a composite unique constraint.<unique cols='key1,key2'/>"
|
|
||||||
}
|
|
||||||
|]
|
|
||||||
|
|
||||||
it "includes m2m relationship markers in the table description" $ do
|
|
||||||
r <- simpleBody <$> get "/"
|
|
||||||
|
|
||||||
let beingDescription = r ^? key "definitions" . key "being" . key "description"
|
|
||||||
|
|
||||||
liftIO $
|
|
||||||
beingDescription `shouldBe` Just
|
|
||||||
[aesonQQ|"<m2m table='part' junction='being_part' source='being' target='part'/>"|]
|
|
||||||
|
|
||||||
describe "Foreign table" $
|
describe "Foreign table" $
|
||||||
|
|
||||||
it "includes foreign table properties" $ do
|
it "includes foreign table properties" $ do
|
||||||
|
|||||||
@@ -120,12 +120,9 @@ spec withConfig = withConfig baseCfg $
|
|||||||
}
|
}
|
||||||
|
|
||||||
context "table with limited privileges" $ do
|
context "table with limited privileges" $ do
|
||||||
it "succeeds deleting the row when return=representation and selecting all columns, returning only the privileged columns" $
|
it "fails deleting the row when return=representation and selecting all the columns" $
|
||||||
request methodDelete "/app_users?id=eq.1" [("Prefer", "return=representation")] mempty
|
request methodDelete "/app_users?id=eq.1" [("Prefer", "return=representation")] mempty
|
||||||
`shouldRespondWith` [json|[ { "id": 1, "email": "test@123.com" } ]|]
|
`shouldRespondWith` 401
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "succeeds deleting the row when return=representation and selecting only the privileged columns" $
|
it "succeeds deleting the row when return=representation and selecting only the privileged columns" $
|
||||||
request methodDelete "/app_users?id=eq.1&select=id,email" [("Prefer", "return=representation")]
|
request methodDelete "/app_users?id=eq.1&select=id,email" [("Prefer", "return=representation")]
|
||||||
|
|||||||
@@ -720,10 +720,11 @@ spec withConfig = withConfig baseCfg $ do
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
it "succeeds inserting if select is not specified, returning only the accessible columns" $
|
it "fails inserting if select is not specified" $
|
||||||
request methodPost "/limited_article_stars" [("Prefer", "return=representation")]
|
request methodPost "/limited_article_stars" [("Prefer", "return=representation")]
|
||||||
[json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` [json|[{"article_id":3,"user_id":1}]|]
|
[json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith`
|
||||||
{ matchStatus = 201
|
[json|{"hint":null,"details":null,"code":"42501","message":"permission denied for view limited_article_stars"}|]
|
||||||
|
{ matchStatus = 401
|
||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,19 +36,6 @@ spec actualPgVersion withConfig = withConfig baseCfg $ do
|
|||||||
, matchHeaders = ["Content-Length" <:> "120"]
|
, matchHeaders = ["Content-Length" <:> "120"]
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "Column-level privileges" $ do
|
|
||||||
it "selects only the accessible columns when no select is specified" $
|
|
||||||
get "/app_users?id=eq.1"
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| [{"id":1,"email":"test@123.com"}] |]
|
|
||||||
{ matchStatus = 200 }
|
|
||||||
|
|
||||||
it "can still select the accessible columns explicitly" $
|
|
||||||
get "/app_users?id=eq.1&select=id,email"
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| [{"id":1,"email":"test@123.com"}] |]
|
|
||||||
{ matchStatus = 200 }
|
|
||||||
|
|
||||||
describe "Filtering response" $ do
|
describe "Filtering response" $ do
|
||||||
it "matches with equality" $
|
it "matches with equality" $
|
||||||
get "/items?id=eq.5"
|
get "/items?id=eq.5"
|
||||||
|
|||||||
Vendored
-3
@@ -28,13 +28,10 @@ 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
-14
@@ -1451,15 +1451,6 @@ create table test.compound_unique(
|
|||||||
unique(key1, key2)
|
unique(key1, key2)
|
||||||
);
|
);
|
||||||
|
|
||||||
create table test.mixed_unique(
|
|
||||||
id integer not null,
|
|
||||||
key1 integer not null,
|
|
||||||
key2 integer not null,
|
|
||||||
value text,
|
|
||||||
unique(id),
|
|
||||||
unique(key1, key2)
|
|
||||||
);
|
|
||||||
|
|
||||||
create table test.family_tree (
|
create table test.family_tree (
|
||||||
id text not null primary key,
|
id text not null primary key,
|
||||||
name text not null,
|
name text not null,
|
||||||
@@ -1935,11 +1926,6 @@ 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