refactor: App.hs and related changes (#1725)

* Use ExceptT to avoid 'staircasing' case analysis in App.hs
* Split large function in App.hs into individual handler functions
* Adapt API of Auth.hs, OpenApi.hs etc. to simplify the use of those modules in App.hs
* Split optional rollback functionality into Middleware
* Unify SimpleError and ApiRequestError into one Error type, so it can be used across modules
This commit is contained in:
Remo Rechkemmer
2021-02-23 22:41:48 +01:00
committed by GitHub
parent 0ddd676ef0
commit e6973f966b
11 changed files with 734 additions and 567 deletions
+1
View File
@@ -81,6 +81,7 @@ library
, jose >= 0.8.1 && < 0.9 , jose >= 0.8.1 && < 0.9
, lens >= 4.14 && < 4.20 , lens >= 4.14 && < 4.20
, lens-aeson >= 1.0.1 && < 1.2 , lens-aeson >= 1.0.1 && < 1.2
, mtl >= 2.2.2 && < 2.3
, network-uri >= 2.6.1 && < 2.8 , network-uri >= 2.6.1 && < 2.8
, optparse-applicative >= 0.13 && < 0.17 , optparse-applicative >= 0.13 && < 0.17
, parsec >= 3.1.11 && < 3.2 , parsec >= 3.1.11 && < 3.2
+499 -382
View File
@@ -9,430 +9,547 @@ Some of its functionality includes:
- Producing HTTP Headers according to RFCs. - Producing HTTP Headers according to RFCs.
- Content Negotiation - Content Negotiation
-} -}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE MultiWayIf #-} module PostgREST.App (postgrest) where
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
module PostgREST.App (
postgrest
) where
import qualified Data.ByteString.Char8 as BS
import qualified Data.HashMap.Strict as M
import qualified Data.List as L (union)
import qualified Data.Set as S
import qualified Hasql.Pool as P
import qualified Hasql.Transaction as H
import qualified Hasql.Transaction as HT
import qualified Hasql.Transaction.Sessions as HT
import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft)
import Data.IORef (IORef, readIORef) import Data.IORef (IORef, readIORef)
import Data.List (union)
import Data.Time.Clock (UTCTime) import Data.Time.Clock (UTCTime)
import Network.HTTP.Types.URI (renderSimpleQuery)
import Control.Applicative import qualified Data.ByteString.Char8 as BS8
import Data.Maybe import qualified Data.ByteString.Lazy as LBS
import Network.HTTP.Types.Header import qualified Data.Set as Set
import Network.HTTP.Types.Status import qualified Hasql.DynamicStatements.Snippet as SQL
import Network.Wai import qualified Hasql.Pool as SQL
import qualified Hasql.Transaction as SQL
import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.HTTP.Types.URI as HTTP
import qualified Network.Wai as Wai
import qualified PostgREST.ApiRequest as ApiRequest
import qualified PostgREST.Auth as Auth
import qualified PostgREST.DbRequestBuilder as ReqBuilder
import qualified PostgREST.DbStructure as DbStructure
import qualified PostgREST.Error as Error
import qualified PostgREST.Middleware as Middleware
import qualified PostgREST.OpenAPI as OpenAPI
import qualified PostgREST.QueryBuilder as QueryBuilder
import qualified PostgREST.RangeQuery as RangeQuery
import qualified PostgREST.Statements as Statements
import PostgREST.ApiRequest (Action (..), ApiRequest (..), import PostgREST.ApiRequest (Action (..), ApiRequest (..),
InvokeMethod (..), Target (..), InvokeMethod (..), Target (..))
mutuallyAgreeable, userApiRequest)
import PostgREST.Auth (attemptJwtClaims, containsRole,
jwtClaims)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.DbRequestBuilder (mutateRequest, readRequest, import PostgREST.Error (Error)
returningCols)
import PostgREST.DbStructure
import PostgREST.Error (PgError (..), SimpleError (..),
errorResponseFor, singularityError)
import PostgREST.Middleware
import PostgREST.OpenAPI
import PostgREST.QueryBuilder (limitedQuery, mutateRequestToQuery,
readRequestToCountQuery,
readRequestToQuery,
requestToCallProcQuery)
import PostgREST.RangeQuery (allRange, contentRangeH,
rangeStatusHeader)
import PostgREST.Statements (callProcStatement,
createExplainStatement,
createReadStatement,
createWriteStatement)
import PostgREST.Types import PostgREST.Types
import Protolude hiding (Proxy, intercalate, toS)
import Protolude hiding (Handler, toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
postgrest :: LogLevel -> IORef AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
data RequestContext = RequestContext
{ ctxConfig :: AppConfig
, ctxDbStructure :: DbStructure
, ctxApiRequest :: ApiRequest
, ctxContentType :: ContentType
}
type Handler = ExceptT Error
type DbHandler = Handler SQL.Transaction
-- | PostgREST application
postgrest
:: LogLevel
-> IORef AppConfig
-> IORef (Maybe DbStructure)
-> SQL.Pool
-> IO UTCTime
-> IO () -- ^ Lauch connection worker in a separate thread
-> Wai.Application
postgrest logLev refConf refDbStructure pool getTime connWorker = postgrest logLev refConf refDbStructure pool getTime connWorker =
pgrstMiddleware logLev $ \ req respond -> do Middleware.pgrstMiddleware logLev $
\req respond -> do
time <- getTime time <- getTime
body <- strictRequestBody req
maybeDbStructure <- readIORef refDbStructure
conf <- readIORef refConf conf <- readIORef refConf
case maybeDbStructure of maybeDbStructure <- readIORef refDbStructure
Nothing -> respond . errorResponseFor $ ConnectionLostError
Just dbStructure -> do
response <- do
let apiReq = userApiRequest (configDbSchemas conf) (configDbRootSpec conf) dbStructure req body
case apiReq of
Left err -> return . errorResponseFor $ err
Right apiRequest -> do
-- The jwt must be checked before touching the db.
attempt <- attemptJwtClaims (configJWKS conf) (configJwtAudience conf) (toS $ iJWT apiRequest) time (configJwtRoleClaimKey conf)
case jwtClaims attempt of
Left errJwt -> return . errorResponseFor $ errJwt
Right claims -> do
let let
authed = containsRole claims eitherResponse :: IO (Either Error Wai.Response)
shouldCommit = configDbTxAllowOverride conf && iPreferTransaction apiRequest == Just Commit eitherResponse =
shouldRollback = configDbTxAllowOverride conf && iPreferTransaction apiRequest == Just Rollback runExceptT $ postgrestResponse conf maybeDbStructure pool time req
preferenceApplied
| shouldCommit = addHeadersIfNotIncluded [(hPreferenceApplied, BS.pack (show Commit))] response <- either Error.errorResponseFor identity <$> eitherResponse
| shouldRollback = addHeadersIfNotIncluded [(hPreferenceApplied, BS.pack (show Rollback))]
| otherwise = identity -- Launch the connWorker when the connection is down. The postgrest
handleReq = do -- function can respond successfully (with a stale schema cache) before
when (shouldRollback || (configDbTxRollbackAll conf && not shouldCommit)) HT.condemn -- the connWorker is done.
mapResponseHeaders preferenceApplied <$> runPgLocals conf claims (app dbStructure conf) apiRequest when (Wai.responseStatus response == HTTP.status503) connWorker
dbResp <- P.use pool $ HT.transaction HT.ReadCommitted (txMode apiRequest) handleReq
return $ either (errorResponseFor . PgError authed) identity dbResp
-- Launch the connWorker when the connection is down. The postgrest function can respond successfully(with a stale schema cache) before the connWorker is done.
when (responseStatus response == status503) connWorker
respond response respond response
txMode :: ApiRequest -> HT.Mode postgrestResponse
txMode apiRequest = :: AppConfig
case (iAction apiRequest, iTarget apiRequest) of -> Maybe DbStructure
(ActionRead _ , _) -> HT.Read -> SQL.Pool
(ActionInfo , _) -> HT.Read -> UTCTime
(ActionInspect _ , _) -> HT.Read -> Wai.Request
(ActionInvoke InvGet , _) -> HT.Read -> Handler IO Wai.Response
(ActionInvoke InvHead, _) -> HT.Read postgrestResponse conf@AppConfig{..} maybeDbStructure pool time req = do
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Stable} _) -> HT.Read body <- lift $ Wai.strictRequestBody req
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Immutable} _) -> HT.Read
_ -> HT.Write
app :: DbStructure -> AppConfig -> ApiRequest -> H.Transaction Response dbStructure <-
app dbStructure conf apiRequest = case maybeDbStructure of
let rawContentTypes = (decodeContentType <$> configRawMediaTypes conf) `L.union` [ CTOctetStream, CTTextPlain ] in Just dbStructure ->
case responseContentTypeOrError (iAccepts apiRequest) rawContentTypes (iAction apiRequest) (iTarget apiRequest) of return dbStructure
Left errorResponse -> return errorResponse Nothing ->
Right contentType -> throwError Error.ConnectionLostError
case (iAction apiRequest, iTarget apiRequest) of
(ActionRead headersOnly, TargetIdent (QualifiedIdentifier tSchema tName)) -> apiRequest@ApiRequest{..} <-
case readSqlParts tSchema tName of liftEither . mapLeft Error.ApiRequestError $
Left errorResponse -> return errorResponse ApiRequest.userApiRequest configDbSchemas configDbRootSpec dbStructure req body
Right (q, cq, bField, _) -> do
let cQuery = if estimatedCount -- The JWT must be checked before touching the db
then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed jwtClaims <- Auth.jwtClaims conf (toS iJWT) time
else cq
stm = createReadStatement q cQuery (contentType == CTSingularJSON) shouldCount contentType <-
(contentType == CTTextCSV) bField pgVer prepared case ApiRequest.mutuallyAgreeable (requestContentTypes conf apiRequest) iAccepts of
explStm = createExplainStatement cq prepared Just ct ->
row <- H.statement mempty stm return ct
let (tableTotal, queryTotal, _ , body, gucHeaders, gucStatus) = row Nothing ->
gucs = (,) <$> gucHeaders <*> gucStatus throwError . Error.ContentTypeError $ map toMime iAccepts
case gucs of
Left err -> return $ errorResponseFor err
Right (ghdrs, gstatus) -> do
total <- if | plannedCount -> H.statement mempty explStm
| estimatedCount -> if tableTotal > (fromIntegral <$> maxRows)
then do estTotal <- H.statement mempty explStm
pure $ if estTotal > tableTotal then estTotal else tableTotal
else pure tableTotal
| otherwise -> pure tableTotal
let (rangeStatus, contentRange) = rangeStatusHeader topLevelRange queryTotal total
status = fromMaybe rangeStatus gstatus
headers = addHeadersIfNotIncluded (catMaybes [
Just $ toHeader contentType, Just contentRange,
Just $ contentLocationH tName (iCanonicalQS apiRequest), profileH])
(unwrapGucHeader <$> ghdrs)
rBody = if headersOnly then mempty else toS body
return $
if contentType == CTSingularJSON && queryTotal /= 1
then errorResponseFor . singularityError $ queryTotal
else responseLBS status headers rBody
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName)) ->
case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse
Right (sq, mq) -> do
let pkCols = tablePKCols dbStructure tSchema tName
stm = createWriteStatement sq mq
(contentType == CTSingularJSON) True
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols pgVer prepared
row <- H.statement mempty stm
let (_, queryTotal, fields, body, gucHeaders, gucStatus) = row
gucs = (,) <$> gucHeaders <*> gucStatus
case gucs of
Left err -> return $ errorResponseFor err
Right (ghdrs, gstatus) -> do
let let
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full handleReq apiReq =
then ([Just $ toHeader contentType, profileH], toS body) handleRequest $ RequestContext conf dbStructure apiReq contentType
else ([], mempty)
status = fromMaybe status201 gstatus runDbHandler pool (txMode apiRequest) jwtClaims .
headers = addHeadersIfNotIncluded (catMaybes ([ Middleware.optionalRollback conf apiRequest $
if null fields Middleware.runPgLocals conf jwtClaims handleReq apiRequest
then Nothing
else Just $ locationH tName fields runDbHandler :: SQL.Pool -> SQL.Mode -> Auth.JWTClaims -> DbHandler a -> Handler IO a
, Just $ contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing runDbHandler pool mode jwtClaims handler = do
, if null pkCols && isNothing (iOnConflict apiRequest) dbResp <-
then Nothing lift . SQL.use pool . SQL.transaction SQL.ReadCommitted mode $ runExceptT handler
else (\x -> ("Preference-Applied", BS.pack (show x))) <$> iPreferResolution apiRequest
] ++ ctHeaders)) (unwrapGucHeader <$> ghdrs) resp <-
if contentType == CTSingularJSON && queryTotal /= 1 liftEither . mapLeft Error.PgErr $
then do mapLeft (Error.PgError $ Auth.containsRole jwtClaims) dbResp
HT.condemn
return . errorResponseFor . singularityError $ queryTotal liftEither resp
else
return $ responseLBS status headers rBody handleRequest :: RequestContext -> DbHandler Wai.Response
handleRequest context@(RequestContext _ _ ApiRequest{..} _) =
case (iAction, iTarget) of
(ActionRead headersOnly, TargetIdent identifier) ->
handleRead headersOnly identifier context
(ActionCreate, TargetIdent identifier) ->
handleCreate identifier context
(ActionUpdate, TargetIdent identifier) ->
handleUpdate identifier context
(ActionSingleUpsert, TargetIdent identifier) ->
handleSingleUpsert identifier context
(ActionDelete, TargetIdent identifier) ->
handleDelete identifier context
(ActionInfo, TargetIdent identifier) ->
handleInfo identifier context
(ActionInvoke invMethod, TargetProc proc _) ->
handleInvoke invMethod proc context
(ActionInspect headersOnly, TargetDefaultSpec tSchema) ->
handleOpenApi headersOnly tSchema context
_ ->
throwError Error.NotFound
handleRead :: Bool -> QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleRead headersOnly identifier context@RequestContext{..} = do
req <- readRequest identifier context
bField <- binaryField context req
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName)) ->
case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse
Right (sq, mq) -> do
row <- H.statement mempty $
createWriteStatement sq mq
(contentType == CTSingularJSON) False (contentType == CTTextCSV)
(iPreferRepresentation apiRequest) mempty pgVer prepared
let (_, queryTotal, _, body, gucHeaders, gucStatus) = row
gucs = (,) <$> gucHeaders <*> gucStatus
case gucs of
Left err -> return $ errorResponseFor err
Right (ghdrs, gstatus) -> do
let let
updateIsNoOp = S.null (iColumns apiRequest) ApiRequest{..} = ctxApiRequest
defStatus | queryTotal == 0 && not updateIsNoOp = status404 AppConfig{..} = ctxConfig
| iPreferRepresentation apiRequest == Full = status200 countQuery = QueryBuilder.readRequestToCountQuery req
| otherwise = status204
status = fromMaybe defStatus gstatus (tableTotal, queryTotal, _ , body, gucHeaders, gucStatus) <-
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ if shouldCount then Just queryTotal else Nothing lift . SQL.statement mempty $
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full Statements.createReadStatement
then ([Just $ toHeader contentType, profileH], toS body) (QueryBuilder.readRequestToQuery req)
else ([], mempty) (if iPreferCount == Just EstimatedCount then
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
if contentType == CTSingularJSON && queryTotal /= 1 QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else else
return $ responseLBS status headers rBody countQuery
)
(ctxContentType == CTSingularJSON)
(shouldCount iPreferCount)
(ctxContentType == CTTextCSV)
bField
(pgVersion ctxDbStructure)
configDbPreparedStatements
total <- readTotal ctxConfig ctxApiRequest tableTotal countQuery
response <- liftEither $ gucResponse <$> gucStatus <*> gucHeaders
let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange queryTotal total
headers =
[ contentRange
, ( "Content-Location"
, "/"
<> toS (qiName identifier)
<> if BS8.null iCanonicalQS then mempty else "?" <> toS iCanonicalQS
)
]
++ contentTypeHeaders context
failNotSingular ctxContentType queryTotal . response status headers $
if headersOnly then mempty else toS body
readTotal :: AppConfig -> ApiRequest -> Maybe Int64 -> SQL.Snippet -> DbHandler (Maybe Int64)
readTotal AppConfig{..} ApiRequest{..} tableTotal countQuery =
case iPreferCount of
Just PlannedCount ->
explain
Just EstimatedCount ->
if tableTotal > (fromIntegral <$> configDbMaxRows) then
max tableTotal <$> explain
else
return tableTotal
_ ->
return tableTotal
where
explain =
lift . SQL.statement mempty . Statements.createExplainStatement countQuery $
configDbPreparedStatements
handleCreate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
let
ApiRequest{..} = ctxApiRequest
pkCols = tablePKCols ctxDbStructure qiSchema qiName
WriteQueryResult{..} <- writeQuery identifier True pkCols context
let
response = gucResponse resGucStatus resGucHeaders
headers =
catMaybes
[ if null resFields then
Nothing
else
Just
( HTTP.hLocation
, "/"
<> toS qiName
<> HTTP.renderSimpleQuery True (splitKeyValue <$> resFields)
)
, Just . RangeQuery.contentRangeH 1 0 $
if shouldCount iPreferCount then Just resQueryTotal else Nothing
, if null pkCols && isNothing iOnConflict then
Nothing
else
(\x -> ("Preference-Applied", BS8.pack $ show x)) <$> iPreferResolution
]
failNotSingular ctxContentType resQueryTotal $
if iPreferRepresentation == Full then
response HTTP.status201 (headers ++ contentTypeHeaders context) (toS resBody)
else
response HTTP.status201 headers mempty
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} contentType) = do
WriteQueryResult{..} <- writeQuery identifier False mempty context
let
response = gucResponse resGucStatus resGucHeaders
fullRepr = iPreferRepresentation == Full
updateIsNoOp = Set.null iColumns
status
| resQueryTotal == 0 && not updateIsNoOp = HTTP.status404
| fullRepr = HTTP.status200
| otherwise = HTTP.status204
contentRangeHeader =
RangeQuery.contentRangeH 0 (resQueryTotal - 1) $
if shouldCount iPreferCount then Just resQueryTotal else Nothing
failNotSingular contentType resQueryTotal $
if fullRepr then
response status (contentTypeHeaders context ++ [contentRangeHeader]) (toS resBody)
else
response status [contentRangeHeader] mempty
handleSingleUpsert :: QualifiedIdentifier -> RequestContext-> DbHandler Wai.Response
handleSingleUpsert identifier context@(RequestContext _ _ ApiRequest{..} _) = do
when (iTopLevelRange /= RangeQuery.allRange) $
throwError Error.PutRangeNotAllowedError
WriteQueryResult{..} <- writeQuery identifier False mempty context
let response = gucResponse resGucStatus resGucHeaders
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName)) ->
case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse
Right (sq, mq) ->
if topLevelRange /= allRange
then return . errorResponseFor $ PutRangeNotAllowedError
else do
row <- H.statement mempty $
createWriteStatement sq mq (contentType == CTSingularJSON) False
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) mempty pgVer prepared
let (_, queryTotal, _, body, gucHeaders, gucStatus) = row
gucs = (,) <$> gucHeaders <*> gucStatus
case gucs of
Left err -> return $ errorResponseFor err
Right (ghdrs, gstatus) -> do
let headers = addHeadersIfNotIncluded (catMaybes [Just $ toHeader contentType, profileH]) (unwrapGucHeader <$> ghdrs)
(defStatus, rBody) = if iPreferRepresentation apiRequest == Full then (status200, toS body) else (status204, mempty)
status = fromMaybe defStatus gstatus
-- 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, PUT /items?id=eq.14 { "id" : 2, .. } is rejected -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
-- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done -- PUT /items?id=eq.14 { "id" : 2, .. } is rejected.
if queryTotal /= 1 -- If this condition is not satisfied then nothing is inserted,
then do -- check the WHERE for INSERT in QueryBuilder.hs to see how it's done
HT.condemn when (resQueryTotal /= 1) $ do
return . errorResponseFor $ PutMatchingPkError lift SQL.condemn
throwError Error.PutMatchingPkError
return $
if iPreferRepresentation == Full then
response HTTP.status200 (contentTypeHeaders context) (toS resBody)
else else
return $ responseLBS status headers rBody response HTTP.status204 (contentTypeHeaders context) mempty
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleDelete identifier context@(RequestContext _ _ ApiRequest{..} contentType) = do
WriteQueryResult{..} <- writeQuery identifier False mempty context
(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName)) ->
case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse
Right (sq, mq) -> do
let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False
(contentType == CTTextCSV)
(iPreferRepresentation apiRequest) mempty pgVer prepared
row <- H.statement mempty stm
let (_, queryTotal, _, body, gucHeaders, gucStatus) = row
gucs = (,) <$> gucHeaders <*> gucStatus
case gucs of
Left err -> return $ errorResponseFor err
Right (ghdrs, gstatus) -> do
let let
defStatus = if iPreferRepresentation apiRequest == Full then status200 else status204 response = gucResponse resGucStatus resGucHeaders
status = fromMaybe defStatus gstatus contentRangeHeader =
contentRangeHeader = contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing RangeQuery.contentRangeH 1 0 $
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full if shouldCount iPreferCount then Just resQueryTotal else Nothing
then ([Just $ toHeader contentType, profileH], toS body)
else ([], mempty)
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
if contentType == CTSingularJSON
&& queryTotal /= 1
then do
HT.condemn
return . errorResponseFor . singularityError $ queryTotal
else
return $ responseLBS status headers rBody
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable)) -> failNotSingular contentType resQueryTotal $
let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in if iPreferRepresentation == Full then
case mTable of response HTTP.status200
Nothing -> return notFound (contentTypeHeaders context ++ [contentRangeHeader])
(toS resBody)
else
response HTTP.status204 [contentRangeHeader] mempty
handleInfo :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m Wai.Response
handleInfo identifier RequestContext{..} =
case find tableMatches $ dbTables ctxDbStructure of
Just table -> Just table ->
let allowH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") return $ Wai.responseLBS HTTP.status200 [allOrigins, allowH table] mempty
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header in Nothing ->
return $ responseLBS status200 [allOrigins, allowH] mempty throwError Error.NotFound
where
allOrigins = ("Access-Control-Allow-Origin", "*")
allowH table =
( HTTP.hAllow
, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET"
)
tableMatches table =
tableName table == qiName identifier
&& tableSchema table == qiSchema identifier
(ActionInvoke invMethod, TargetProc proc@ProcDescription{pdSchema, pdName} _) -> handleInvoke :: InvokeMethod -> ProcDescription -> RequestContext -> DbHandler Wai.Response
let tName = fromMaybe pdName $ procTableName proc in handleInvoke invMethod proc context@RequestContext{..} = do
case readSqlParts pdSchema tName of
Left errorResponse -> return errorResponse
Right (q, cq, bField, returning) -> do
let let
preferParams = iPreferParameters apiRequest ApiRequest{..} = ctxApiRequest
pq = requestToCallProcQuery (QualifiedIdentifier pdSchema pdName) (specifiedProcArgs (iColumns apiRequest) proc)
(iPayload apiRequest) returnsScalar preferParams returning identifier =
stm = callProcStatement returnsScalar returnsSingle pq q cq shouldCount (contentType == CTSingularJSON) QualifiedIdentifier
(contentType == CTTextCSV) (preferParams == Just MultipleObjects) bField pgVer prepared (pdSchema proc)
row <- H.statement mempty stm (fromMaybe (pdName proc) $ procTableName proc)
let (tableTotal, queryTotal, body, gucHeaders, gucStatus) = row
gucs = (,) <$> gucHeaders <*> gucStatus returnsSingle (ApiRequest.TargetProc target _) = procReturnsSingle target
case gucs of returnsSingle _ = False
Left err -> return $ errorResponseFor err
Right (ghdrs, gstatus) -> do req <- readRequest identifier context
let (rangeStatus, contentRange) = rangeStatusHeader topLevelRange queryTotal tableTotal bField <- binaryField context req
status = fromMaybe rangeStatus gstatus
headers = addHeadersIfNotIncluded (tableTotal, queryTotal, body, gucHeaders, gucStatus) <-
(catMaybes [Just $ toHeader contentType, Just contentRange, profileH]) lift . SQL.statement mempty $
(unwrapGucHeader <$> ghdrs) Statements.callProcStatement
rBody = if invMethod == InvHead then mempty else toS body (returnsScalar iTarget)
if contentType == CTSingularJSON && queryTotal /= 1 (returnsSingle iTarget)
then do (QueryBuilder.requestToCallProcQuery
HT.condemn (QualifiedIdentifier (pdSchema proc) (pdName proc))
return . errorResponseFor . singularityError $ queryTotal (specifiedProcArgs iColumns proc)
iPayload
(returnsScalar iTarget)
iPreferParameters
(ReqBuilder.returningCols req [])
)
(QueryBuilder.readRequestToQuery req)
(QueryBuilder.readRequestToCountQuery req)
(shouldCount iPreferCount)
(ctxContentType == CTSingularJSON)
(ctxContentType == CTTextCSV)
(iPreferParameters == Just MultipleObjects)
bField
(pgVersion ctxDbStructure)
(configDbPreparedStatements ctxConfig)
response <- liftEither $ gucResponse <$> gucStatus <*> gucHeaders
let
(status, contentRange) =
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
failNotSingular ctxContentType queryTotal $
response status
(contentTypeHeaders context ++ [contentRange])
(if invMethod == InvHead then mempty else toS body)
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest _) = do
body <-
lift $
OpenAPI.encode conf dbStructure
<$> SQL.statement tSchema (DbStructure.accessibleTables configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
<*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements)
return $
Wai.responseLBS HTTP.status200
(toHeader CTOpenAPI : maybeToList (profileHeader apiRequest))
(if headersOnly then mempty else toS body)
txMode :: ApiRequest -> SQL.Mode
txMode ApiRequest{..} =
case (iAction, iTarget) of
(ActionRead _, _) ->
SQL.Read
(ActionInfo, _) ->
SQL.Read
(ActionInspect _, _) ->
SQL.Read
(ActionInvoke InvGet, _) ->
SQL.Read
(ActionInvoke InvHead, _) ->
SQL.Read
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Stable} _) ->
SQL.Read
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Immutable} _) ->
SQL.Read
_ ->
SQL.Write
-- | Result from executing a write query on the database
data WriteQueryResult = WriteQueryResult
{ resQueryTotal :: Int64
, resFields :: [ByteString]
, resBody :: ByteString
, resGucStatus :: Maybe HTTP.Status
, resGucHeaders :: [GucHeader]
}
writeQuery :: QualifiedIdentifier -> Bool -> [Text] -> RequestContext -> DbHandler WriteQueryResult
writeQuery identifier@QualifiedIdentifier{..} isInsert pkCols context@RequestContext{..} = do
readReq <- readRequest identifier context
mutateReq <-
liftEither $
ReqBuilder.mutateRequest qiSchema qiName ctxApiRequest
(tablePKCols ctxDbStructure qiSchema qiName)
readReq
(_, queryTotal, fields, body, gucHeaders, gucStatus) <-
lift . SQL.statement mempty $
Statements.createWriteStatement
(QueryBuilder.readRequestToQuery readReq)
(QueryBuilder.mutateRequestToQuery mutateReq)
(ctxContentType == CTSingularJSON)
isInsert
(ctxContentType == CTTextCSV)
(iPreferRepresentation ctxApiRequest)
pkCols
(pgVersion ctxDbStructure)
(configDbPreparedStatements ctxConfig)
liftEither $ WriteQueryResult queryTotal fields body <$> gucStatus <*> gucHeaders
-- | Response with headers and status overridden from GUCs.
gucResponse
:: Maybe HTTP.Status
-> [GucHeader]
-> HTTP.Status
-> [HTTP.Header]
-> LBS.ByteString
-> Wai.Response
gucResponse gucStatus gucHeaders status headers =
Wai.responseLBS (fromMaybe status gucStatus) $
addHeadersIfNotIncluded headers (map unwrapGucHeader gucHeaders)
-- |
-- Fail a response if a single JSON object was requested and not exactly one
-- was found.
failNotSingular :: ContentType -> Int64 -> Wai.Response -> DbHandler Wai.Response
failNotSingular contentType queryTotal response =
if contentType == CTSingularJSON && queryTotal /= 1 then
do
lift SQL.condemn
throwError $ Error.singularityError queryTotal
else else
return $ responseLBS status headers rBody return response
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do shouldCount :: Maybe PreferCount -> Bool
let host = configServerHost conf shouldCount preferCount =
port = toInteger $ configServerPort conf preferCount == Just ExactCount || preferCount == Just EstimatedCount
proxy = pickProxy $ toS <$> configOpenApiServerProxyUri conf
uri Nothing = ("http", host, port, "/")
uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b)
uri' = uri proxy
toTableInfo :: [Table] -> [(Table, [Column], [Text])]
toTableInfo = map (\t -> let (s, tn) = (tableSchema t, tableName t) in (t, tableCols dbStructure s tn, tablePKCols dbStructure s tn))
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd $ dbPrimaryKeys dbStructure
body <- encodeApi <$> returnsScalar :: ApiRequest.Target -> Bool
H.statement tSchema (accessibleTables prepared) <*> returnsScalar (TargetProc proc _) = procReturnsScalar proc
H.statement tSchema (schemaDescription prepared) <*> returnsScalar _ = False
H.statement tSchema (accessibleProcs prepared)
return $ responseLBS status200 (catMaybes [Just $ toHeader CTOpenAPI, profileH]) (if headersOnly then mempty else toS body)
_ -> return notFound readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest
readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure apiRequest _) =
liftEither $
ReqBuilder.readRequest qiSchema qiName configDbMaxRows
(dbRelations dbStructure)
apiRequest
where contentTypeHeaders :: RequestContext -> [HTTP.Header]
notFound = responseLBS status404 mempty "" contentTypeHeaders RequestContext{..} =
maxRows = configDbMaxRows conf toHeader ctxContentType : maybeToList (profileHeader ctxApiRequest)
prepared = configDbPreparedStatements conf
exactCount = iPreferCount apiRequest == Just ExactCount
estimatedCount = iPreferCount apiRequest == Just EstimatedCount
plannedCount = iPreferCount apiRequest == Just PlannedCount
shouldCount = exactCount || estimatedCount
topLevelRange = iTopLevelRange apiRequest
returnsScalar =
case iTarget apiRequest of
TargetProc proc _ -> procReturnsScalar proc
_ -> False
returnsSingle =
case iTarget apiRequest of
TargetProc proc _ -> procReturnsSingle proc
_ -> False
pgVer = pgVersion dbStructure
profileH = contentProfileH <$> iProfile apiRequest
readSqlParts s t = requestContentTypes :: AppConfig -> ApiRequest -> [ContentType]
let requestContentTypes conf ApiRequest{..} =
readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest case iAction of
returnings :: ReadRequest -> Either Response [FieldName] ActionRead _ -> defaultContentTypes ++ rawContentTypes conf
returnings rr = Right (returningCols rr []) ActionInvoke _ -> invokeContentTypes
in
(,,,) <$>
(readRequestToQuery <$> readReq) <*>
(readRequestToCountQuery <$> readReq) <*>
(binaryField contentType rawContentTypes returnsScalar =<< readReq) <*>
(returnings =<< readReq)
mutateSqlParts s t =
let
readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest
mutReq = mutateRequest s t apiRequest (tablePKCols dbStructure s t) =<< readReq
in
(,) <$>
(readRequestToQuery <$> readReq) <*>
(mutateRequestToQuery <$> mutReq)
responseContentTypeOrError :: [ContentType] -> [ContentType] -> Action -> Target -> Either Response ContentType
responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts
where
contentTypesForRequest = case action of
ActionRead _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
++ rawContentTypes
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
++ rawContentTypes
++ [CTOpenAPI | tpIsRootSpec target]
ActionInspect _ -> [CTOpenAPI, CTApplicationJSON] ActionInspect _ -> [CTOpenAPI, CTApplicationJSON]
ActionInfo -> [CTTextCSV] ActionInfo -> [CTTextCSV]
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] _ -> defaultContentTypes
serves sProduces cAccepts =
case mutuallyAgreeable sProduces cAccepts of
Nothing -> Left . errorResponseFor . ContentTypeError . map toMime $ cAccepts
Just ct -> Right ct
{-
| If raw(binary) output is requested, check that ContentType is one of the admitted rawContentTypes and that
| `?select=...` contains only one field other than `*`
-}
binaryField :: ContentType -> [ContentType] -> Bool -> ReadRequest -> Either Response (Maybe FieldName)
binaryField ct rawContentTypes isScalarProc readReq
| isScalarProc =
if ct `elem` rawContentTypes
then Right $ Just "pgrst_scalar"
else Right Nothing
| ct `elem` rawContentTypes =
let fieldName = headMay fldNames in
if length fldNames == 1 && fieldName /= Just "*"
then Right fieldName
else Left . errorResponseFor $ BinaryFieldError ct
| otherwise = Right Nothing
where where
fldNames = fstFieldNames readReq invokeContentTypes =
defaultContentTypes
++ rawContentTypes conf
++ [CTOpenAPI | ApiRequest.tpIsRootSpec iTarget]
defaultContentTypes =
[CTApplicationJSON, CTSingularJSON, CTTextCSV]
locationH :: TableName -> [BS.ByteString] -> Header -- |
locationH tName fields = -- If raw(binary) output is requested, check that ContentType is one of the admitted
-- rawContentTypes and that`?select=...` contains only one field other than `*`
binaryField :: Monad m => RequestContext -> ReadRequest -> Handler m (Maybe FieldName)
binaryField RequestContext{..} readReq
| returnsScalar (iTarget ctxApiRequest) && ctxContentType `elem` rawContentTypes ctxConfig =
return $ Just "pgrst_scalar"
| ctxContentType `elem` rawContentTypes ctxConfig =
let let
locationFields = renderSimpleQuery True $ splitKeyValue <$> fields fldNames = fstFieldNames readReq
fieldName = headMay fldNames
in in
(hLocation, "/" <> toS tName <> locationFields) if length fldNames == 1 && fieldName /= Just "*" then
where return fieldName
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString) else
throwError $ Error.BinaryFieldError ctxContentType
| otherwise =
return Nothing
rawContentTypes :: AppConfig -> [ContentType]
rawContentTypes AppConfig{..} =
(decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain]
profileHeader :: ApiRequest -> Maybe HTTP.Header
profileHeader ApiRequest{..} =
(,) "Content-Profile" <$> (toS <$> iProfile)
splitKeyValue :: ByteString -> (ByteString, ByteString)
splitKeyValue kv = splitKeyValue kv =
let (k, v) = BS.break (== '=') kv (k, BS8.tail v)
in (k, BS.tail v) where
(k, v) = BS8.break (== '=') kv
contentLocationH :: TableName -> ByteString -> Header
contentLocationH tName qString =
("Content-Location", "/" <> toS tName <> if BS.null qString then mempty else "?" <> toS qString)
contentProfileH :: Schema -> Header
contentProfileH schema =
("Content-Profile", toS schema)
+46 -86
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-| {-|
Module : PostgREST.Auth Module : PostgREST.Auth
Description : PostgREST authorization functions. Description : PostgREST authorization functions.
@@ -12,74 +10,63 @@ Authentication should always be implemented in an external service.
In the test suite there is an example of simple login function that can be used for a In the test suite there is an example of simple login function that can be used for a
very simple authentication system inside the PostgreSQL database. very simple authentication system inside the PostgreSQL database.
-} -}
module PostgREST.Auth ( {-# LANGUAGE RecordWildCards #-}
containsRole module PostgREST.Auth (containsRole, jwtClaims, JWTClaims) where
, jwtClaims
, attemptJwtClaims
, parseSecret
) where
import qualified Crypto.JOSE.Types as JOSE.Types import qualified Crypto.JWT as JWT
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import Data.Vector as V import qualified Data.Vector as V
import Control.Lens (set) import Control.Lens (set)
import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft)
import Data.Time.Clock (UTCTime) import Data.Time.Clock (UTCTime)
import Crypto.JWT import PostgREST.Config (AppConfig (..))
import PostgREST.Error (Error (..))
import PostgREST.Types (JSPath, JSPathExp (..))
import PostgREST.Error (SimpleError (..)) import Protolude
import PostgREST.Types
import Protolude hiding (toS)
import Protolude.Conv (toS)
{-|
Possible situations encountered with client JWTs
-}
data JWTAttempt = JWTInvalid JWTError
| JWTMissingSecret
| JWTClaims (M.HashMap Text JSON.Value)
jwtClaims :: JWTAttempt -> Either SimpleError (M.HashMap Text JSON.Value) type JWTClaims = M.HashMap Text JSON.Value
jwtClaims attempt =
case attempt of
JWTMissingSecret -> Left JwtTokenMissing
JWTInvalid JWTExpired -> Left $ JwtTokenInvalid "JWT expired"
JWTInvalid e -> Left $ JwtTokenInvalid $ show e
JWTClaims claims -> Right claims
{-| -- | Receives the JWT secret and audience (from config) and a JWT and returns a
Receives the JWT secret and audience (from config) and a JWT and returns a map -- map of JWT claims.
of JWT claims. jwtClaims :: Monad m =>
-} AppConfig -> LByteString -> UTCTime -> ExceptT Error m JWTClaims
attemptJwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> JSPath -> IO JWTAttempt jwtClaims _ "" _ = return M.empty
attemptJwtClaims _ _ "" _ _ = return $ JWTClaims M.empty jwtClaims AppConfig{..} payload time = do
attemptJwtClaims maybeSecret audience payload time jspath = secret <- liftEither . maybeToRight JwtTokenMissing $ configJWKS
case maybeSecret of eitherClaims <-
Nothing -> return JWTMissingSecret lift . runExceptT $
Just secret -> do JWT.verifyClaimsAt validation secret time =<< JWT.decodeCompact payload
let validation = set allowedSkew 1 $ defaultJWTValidationSettings (maybe (const True) (==) audience) liftEither . mapLeft jwtClaimsError $ claimsMap configJwtRoleClaimKey <$> eitherClaims
eJwt <- runExceptT $ do where
jwt <- decodeCompact payload validation =
verifyClaimsAt validation secret time jwt JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 1
return $ case eJwt of
Left e -> JWTInvalid e
Right jwt -> JWTClaims $ claims2map jwt jspath
{-| audienceCheck :: JWT.StringOrURI -> Bool
Turn JWT ClaimSet into something easier to work with, audienceCheck = maybe (const True) (==) configJwtAudience
also here the jspath is applied to put the "role" in the map
-} jwtClaimsError :: JWT.JWTError -> Error
claims2map :: ClaimsSet -> JSPath -> M.HashMap Text JSON.Value jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
claims2map claims jspath = (\case jwtClaimsError e = JwtTokenInvalid $ show e
-- | Turn JWT ClaimSet into something easier to work with.
--
-- Also, here the jspath is applied to put the "role" in the map.
claimsMap :: JSPath -> JWT.ClaimsSet -> JWTClaims
claimsMap jspath claims =
case JSON.toJSON claims of
val@(JSON.Object o) -> val@(JSON.Object o) ->
let role = maybe M.empty (M.singleton "role") $ M.delete "role" o `M.union` role val
walkJSPath (Just val) jspath in _ ->
M.delete "role" o `M.union` role -- mutating the map M.empty
_ -> M.empty where
) $ JSON.toJSON claims role value =
maybe M.empty (M.singleton "role") $ walkJSPath (Just value) jspath
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
walkJSPath x [] = x walkJSPath x [] = x
@@ -87,33 +74,6 @@ walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (M.lookup key o
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
walkJSPath _ _ = Nothing walkJSPath _ _ = Nothing
{-| -- | Whether a response from jwtClaims contains a role claim
Whether a response from jwtClaims contains a role claim containsRole :: JWTClaims -> Bool
-}
containsRole :: M.HashMap Text JSON.Value -> Bool
containsRole = M.member "role" containsRole = M.member "role"
{-|
Parse `jwt-secret` configuration option and turn into a JWKSet.
There are three ways to specify `jwt-secret`: text secret, JSON Web Key
(JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
with one key and the last is converted as is.
-}
parseSecret :: ByteString -> JWKSet
parseSecret str =
fromMaybe (maybe secret (\jwk' -> JWKSet [jwk']) maybeJWK)
maybeJWKSet
where
maybeJWKSet = JSON.decode (toS str) :: Maybe JWKSet
maybeJWK = JSON.decode (toS str) :: Maybe JWK
secret = JWKSet [jwkFromSecret str]
{-|
Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret.
-}
jwkFromSecret :: ByteString -> JWK
jwkFromSecret key =
fromKeyMaterial km
where
km = OctKeyMaterial (OctKeyParameters (JOSE.Types.Base64Octets key))
+27 -6
View File
@@ -20,7 +20,8 @@ Other hardcoded options such as the minimum version number also belong here.
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-} {-# OPTIONS_GHC -fno-warn-type-defaults #-}
module PostgREST.Config ( prettyVersion module PostgREST.Config
( prettyVersion
, docsVersion , docsVersion
, CLI (..) , CLI (..)
, Command (..) , Command (..)
@@ -31,9 +32,12 @@ module PostgREST.Config ( prettyVersion
, readCLIShowHelp , readCLIShowHelp
, readEnvironment , readEnvironment
, readConfig , readConfig
) , parseSecret
where ) where
import qualified Crypto.JOSE.Types as JOSE
import qualified Crypto.JWT as JWT
import qualified Data.Aeson as JSON
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
@@ -42,7 +46,7 @@ import qualified Data.Map.Strict as M
import Control.Lens (preview) import Control.Lens (preview)
import Control.Monad (fail) import Control.Monad (fail)
import Crypto.JWT (JWKSet, StringOrURI, stringOrUri) import Crypto.JWT (JWK, JWKSet, StringOrURI, stringOrUri)
import Data.Aeson (encode, toJSON) import Data.Aeson (encode, toJSON)
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft)
import Data.List (lookup) import Data.List (lookup)
@@ -63,9 +67,8 @@ import System.Posix.Types (FileMode)
import Control.Applicative import Control.Applicative
import Data.Monoid import Data.Monoid
import Options.Applicative hiding (str) import Options.Applicative hiding (str)
import Text.Heredoc import Text.Heredoc (str)
import PostgREST.Auth (parseSecret)
import PostgREST.Parsers (pRoleClaimKey) import PostgREST.Parsers (pRoleClaimKey)
import PostgREST.Private.ProxyUri (isMalformedProxyUri) import PostgREST.Private.ProxyUri (isMalformedProxyUri)
import PostgREST.Types (JSPath, JSPathExp (..), import PostgREST.Types (JSPath, JSPathExp (..),
@@ -593,3 +596,21 @@ loadDbUriFile conf = extractDbUri mDbUri
Nothing -> return dbUri Nothing -> return dbUri
Just filename -> strip <$> readFile (toS filename) Just filename -> strip <$> readFile (toS filename)
setDbUri dbUri = conf {configDbUri = dbUri} setDbUri dbUri = conf {configDbUri = dbUri}
{-|
Parse `jwt-secret` configuration option and turn into a JWKSet.
There are three ways to specify `jwt-secret`: text secret, JSON Web Key
(JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
with one key and the last is converted as is.
-}
parseSecret :: ByteString -> JWKSet
parseSecret bytes =
fromMaybe (maybe secret (\jwk' -> JWT.JWKSet [jwk']) maybeJWK)
maybeJWKSet
where
maybeJWKSet = JSON.decode (toS bytes) :: Maybe JWKSet
maybeJWK = JSON.decode (toS bytes) :: Maybe JWK
secret = JWT.JWKSet [JWT.fromKeyMaterial keyMaterial]
keyMaterial = JWT.OctKeyMaterial . JWT.OctKeyParameters $ JOSE.Base64Octets bytes
+5 -7
View File
@@ -7,7 +7,6 @@ This module is in charge of building an intermediate representation(ReadRequest,
A query tree is built in case of resource embedding. By inferring the relationship between tables, join conditions are added for every embedded resource. A query tree is built in case of resource embedding. By inferring the relationship between tables, join conditions are added for every embedded resource.
-} -}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
@@ -28,10 +27,9 @@ import Data.Text (isInfixOf)
import Control.Applicative import Control.Applicative
import Data.Tree import Data.Tree
import Network.Wai
import PostgREST.ApiRequest (Action (..), ApiRequest (..)) import PostgREST.ApiRequest (Action (..), ApiRequest (..))
import PostgREST.Error (ApiRequestError (..), errorResponseFor) import PostgREST.Error (ApiRequestError (..), Error (..))
import PostgREST.Parsers import PostgREST.Parsers
import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange) import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange)
import PostgREST.Types import PostgREST.Types
@@ -40,9 +38,9 @@ import Protolude hiding (from)
-- | Builds the ReadRequest tree on a number of stages. -- | Builds the ReadRequest 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.
readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest -> Either Response ReadRequest readRequest :: Schema -> TableName -> Maybe Integer -> [Relation] -> ApiRequest -> Either Error ReadRequest
readRequest schema rootTableName maxRows allRels apiRequest = readRequest schema rootTableName maxRows allRels apiRequest =
mapLeft errorResponseFor $ mapLeft ApiRequestError $
treeRestrictRange maxRows =<< treeRestrictRange maxRows =<<
augmentRequestWithJoin schema rootRels =<< augmentRequestWithJoin schema rootRels =<<
addFiltersOrdersRanges apiRequest =<< addFiltersOrdersRanges apiRequest =<<
@@ -281,8 +279,8 @@ addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
where where
pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
mutateRequest :: Schema -> TableName -> ApiRequest -> [FieldName] -> ReadRequest -> Either Response MutateRequest mutateRequest :: Schema -> TableName -> ApiRequest -> [FieldName] -> ReadRequest -> Either Error MutateRequest
mutateRequest schema tName apiRequest pkCols readReq = mapLeft errorResponseFor $ mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
case action of case action of
ActionCreate -> do ActionCreate -> do
confCols <- case iOnConflict apiRequest of confCols <- case iOnConflict apiRequest of
+16 -6
View File
@@ -3,14 +3,13 @@ Module : PostgREST.Error
Description : PostgREST error HTTP responses Description : PostgREST error HTTP responses
-} -}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module PostgREST.Error ( module PostgREST.Error (
errorResponseFor errorResponseFor
, ApiRequestError(..) , ApiRequestError(..)
, PgError(..) , PgError(..)
, SimpleError(..) , Error(..)
, errorPayload , errorPayload
, checkIsFatal , checkIsFatal
, singularityError , singularityError
@@ -220,7 +219,7 @@ checkIsFatal (PgError _ (P.SessionError (H.QueryError _ _ (H.ResultError (H.Serv
checkIsFatal _ = Nothing checkIsFatal _ = Nothing
data SimpleError data Error
= GucHeadersError = GucHeadersError
| GucStatusError | GucStatusError
| BinaryFieldError ContentType | BinaryFieldError ContentType
@@ -231,8 +230,11 @@ data SimpleError
| JwtTokenInvalid Text | JwtTokenInvalid Text
| SingularityError Integer | SingularityError Integer
| ContentTypeError [ByteString] | ContentTypeError [ByteString]
| NotFound
| ApiRequestError ApiRequestError
| PgErr PgError
instance PgrstError SimpleError where instance PgrstError Error where
status GucHeadersError = HT.status500 status GucHeadersError = HT.status500
status GucStatusError = HT.status500 status GucStatusError = HT.status500
status (BinaryFieldError _) = HT.status406 status (BinaryFieldError _) = HT.status406
@@ -243,12 +245,17 @@ instance PgrstError SimpleError where
status (JwtTokenInvalid _) = HT.unauthorized401 status (JwtTokenInvalid _) = HT.unauthorized401
status (SingularityError _) = HT.status406 status (SingularityError _) = HT.status406
status (ContentTypeError _) = HT.status415 status (ContentTypeError _) = HT.status415
status NotFound = HT.status404
status (PgErr err) = status err
status (ApiRequestError err) = status err
headers (SingularityError _) = [toHeader CTSingularJSON] headers (SingularityError _) = [toHeader CTSingularJSON]
headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m] headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m]
headers (PgErr err) = headers err
headers (ApiRequestError err) = headers err
headers _ = [toHeader CTApplicationJSON] headers _ = [toHeader CTApplicationJSON]
instance JSON.ToJSON SimpleError where instance JSON.ToJSON Error where
toJSON GucHeadersError = JSON.object [ toJSON GucHeadersError = JSON.object [
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)] "message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)]
toJSON GucStatusError = JSON.object [ toJSON GucStatusError = JSON.object [
@@ -273,10 +280,13 @@ instance JSON.ToJSON SimpleError where
"message" .= ("Server lacks JWT secret" :: Text)] "message" .= ("Server lacks JWT secret" :: Text)]
toJSON (JwtTokenInvalid message) = JSON.object [ toJSON (JwtTokenInvalid message) = JSON.object [
"message" .= (message :: Text)] "message" .= (message :: Text)]
toJSON NotFound = JSON.object []
toJSON (PgErr err) = JSON.toJSON err
toJSON (ApiRequestError err) = JSON.toJSON err
invalidTokenHeader :: Text -> Header invalidTokenHeader :: Text -> Header
invalidTokenHeader m = invalidTokenHeader m =
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m)) ("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m))
singularityError :: (Integral a) => a -> SimpleError singularityError :: (Integral a) => a -> Error
singularityError = SingularityError . toInteger singularityError = SingularityError . toInteger
+45 -9
View File
@@ -2,11 +2,15 @@
Module : PostgREST.Middleware Module : PostgREST.Middleware
Description : Sets CORS policy. Also the PostgreSQL GUCs, role, search_path and pre-request function. Description : Sets CORS policy. Also the PostgreSQL GUCs, role, search_path and pre-request function.
-} -}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE FlexibleContexts #-} module PostgREST.Middleware
{-# LANGUAGE ScopedTypeVariables #-} ( runPgLocals
, pgrstFormat
module PostgREST.Middleware where , pgrstMiddleware
, defaultCorsPolicy
, corsPolicy
, optionalRollback
) where
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.DynamicStatements.Statement as H import qualified Hasql.DynamicStatements.Statement as H
@@ -23,8 +27,10 @@ import Data.Scientific (FPFormat (..),
isInteger) isInteger)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Hasql.Transaction as H import qualified Hasql.Transaction as H
import qualified Network.HTTP.Types.Header as HTTP
import Network.HTTP.Types.Status (Status, status400, import Network.HTTP.Types.Status (Status, status400,
status500, statusCode) status500, statusCode)
import qualified Network.Wai as Wai
import Network.Wai.Logger (showSockAddr) import Network.Wai.Logger (showSockAddr)
import System.Log.FastLogger (toLogStr) import System.Log.FastLogger (toLogStr)
@@ -35,8 +41,11 @@ import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.RequestLogger import Network.Wai.Middleware.RequestLogger
import Network.Wai.Middleware.Static (only, staticPolicy) import Network.Wai.Middleware.Static (only, staticPolicy)
import qualified PostgREST.Types as Types
import PostgREST.ApiRequest (ApiRequest (..)) import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Error (Error, errorResponseFor)
import PostgREST.QueryBuilder (setConfigLocal) import PostgREST.QueryBuilder (setConfigLocal)
import PostgREST.Types (LogLevel (..)) import PostgREST.Types (LogLevel (..))
import Protolude hiding (head, toS) import Protolude hiding (head, toS)
@@ -45,13 +54,13 @@ import System.IO.Unsafe (unsafePerformIO)
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function -- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
runPgLocals :: AppConfig -> M.HashMap Text JSON.Value -> runPgLocals :: AppConfig -> M.HashMap Text JSON.Value ->
(ApiRequest -> H.Transaction Response) -> (ApiRequest -> ExceptT Error H.Transaction Response) ->
ApiRequest -> H.Transaction Response ApiRequest -> ExceptT Error H.Transaction Response
runPgLocals conf claims app req = do runPgLocals conf claims app req = do
H.statement mempty $ H.dynamicallyParameterized lift $ H.statement mempty $ H.dynamicallyParameterized
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql)) ("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql))
HD.noResult (configDbPreparedStatements conf) HD.noResult (configDbPreparedStatements conf)
traverse_ H.sql preReqSql lift $ traverse_ H.sql preReqSql
app req app req
where where
methodSql = setConfigLocal mempty ("request.method", toS $ iMethod req) methodSql = setConfigLocal mempty ("request.method", toS $ iMethod req)
@@ -140,3 +149,30 @@ unquoted (JSON.Number n) =
toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
unquoted (JSON.Bool b) = show b unquoted (JSON.Bool b) = show b
unquoted v = toS $ JSON.encode v unquoted v = toS $ JSON.encode v
-- | Set a transaction to eventually roll back if requested and set respective
-- headers on the response.
optionalRollback
:: AppConfig
-> ApiRequest
-> ExceptT Error H.Transaction Wai.Response
-> ExceptT Error H.Transaction Wai.Response
optionalRollback AppConfig{..} ApiRequest{..} transaction = do
resp <- catchError transaction $ return . errorResponseFor
when (shouldRollback || (configDbTxRollbackAll && not shouldCommit))
(lift H.condemn)
return $ Wai.mapResponseHeaders preferenceApplied resp
where
shouldCommit =
configDbTxAllowOverride && iPreferTransaction == Just Types.Commit
shouldRollback =
configDbTxAllowOverride && iPreferTransaction == Just Types.Rollback
preferenceApplied
| shouldCommit =
Types.addHeadersIfNotIncluded
[(HTTP.hPreferenceApplied, BS.pack (show Types.Commit))]
| shouldRollback =
Types.addHeadersIfNotIncluded
[(HTTP.hPreferenceApplied, BS.pack (show Types.Rollback))]
| otherwise =
identity
+41 -18
View File
@@ -2,17 +2,15 @@
Module : PostgREST.OpenAPI Module : PostgREST.OpenAPI
Description : Generates the OpenAPI output Description : Generates the OpenAPI output
-} -}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}
module PostgREST.OpenAPI (encode) where
module PostgREST.OpenAPI (
encodeOpenAPI
, pickProxy
) where
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashSet.InsOrd as Set import qualified Data.HashSet.InsOrd as Set
import Control.Arrow ((&&&)) import Control.Arrow ((&&&))
import Data.Aeson (decode, encode)
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList) import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.String (IsString (..)) import Data.String (IsString (..))
@@ -25,16 +23,29 @@ import Control.Lens
import Data.Swagger import Data.Swagger
import PostgREST.ApiRequest (ContentType (..)) import PostgREST.ApiRequest (ContentType (..))
import PostgREST.Config (docsVersion, prettyVersion) import PostgREST.Config (AppConfig (..), docsVersion,
prettyVersion)
import PostgREST.Private.ProxyUri (isMalformedProxyUri, toURI) import PostgREST.Private.ProxyUri (isMalformedProxyUri, toURI)
import PostgREST.Types (Column (..), ForeignKey (..), import PostgREST.Types (Column (..), DbStructure (..),
PgArg (..), PrimaryKey (..), ForeignKey (..), PgArg (..),
PrimaryKey (..),
ProcDescription (..), Proxy (..), ProcDescription (..), Proxy (..),
Table (..), toMime) Table (..), tableCols, tableName,
tablePKCols, tableSchema, toMime)
import Protolude hiding (Proxy, dropWhile, get, import Protolude hiding (Proxy, dropWhile, get,
intercalate, toLower, toS, (&)) intercalate, toLower, toS, (&))
import Protolude.Conv (toS) import Protolude.Conv (toS)
encode :: AppConfig -> DbStructure -> [Table] -> Maybe Text -> HashMap.HashMap k [ProcDescription] -> LBS.ByteString
encode conf dbStructure tables schemaDescription procs =
JSON.encode $
postgrestSpec
(concat $ HashMap.elems procs)
(openApiTableInfo dbStructure <$> tables)
(proxyUri conf)
schemaDescription
(dbPrimaryKeys dbStructure)
makeMimeList :: [ContentType] -> MimeList makeMimeList :: [ContentType] -> MimeList
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
@@ -63,7 +74,7 @@ makeTableDef pks (t, cs, _) =
makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema) makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema)
makeProperty pks c = (colName c, Inline s) makeProperty pks c = (colName c, Inline s)
where where
e = if null $ colEnum c then Nothing else decode $ encode $ colEnum c e = if null $ colEnum c then Nothing else JSON.decode $ JSON.encode $ colEnum c
fk ForeignKey{fkCol=Column{colTable=Table{tableName=a}, colName=b}} = fk ForeignKey{fkCol=Column{colTable=Table{tableName=a}, colName=b}} =
intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"] intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]
pk :: Bool pk :: Bool
@@ -80,7 +91,7 @@ makeProperty pks c = (colName c, Inline s)
colDescription c colDescription c
s = s =
(mempty :: Schema) (mempty :: Schema)
& default_ .~ (decode . toS =<< colDefault c) & default_ .~ (JSON.decode . toS =<< colDefault c)
& description .~ d & description .~ d
& enum_ .~ e & enum_ .~ e
& format ?~ colType c & format ?~ colType c
@@ -111,7 +122,7 @@ makePreferParam ts =
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader & in_ .~ ParamHeader
& type_ ?~ SwaggerString & type_ ?~ SwaggerString
& enum_ .~ decode (encode ts)) & enum_ .~ JSON.decode (JSON.encode ts))
makeProcParam :: ProcDescription -> [Referenced Param] makeProcParam :: ProcDescription -> [Referenced Param]
makeProcParam pd = makeProcParam pd =
@@ -162,7 +173,7 @@ makeParamDefs ti =
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader & in_ .~ ParamHeader
& type_ ?~ SwaggerString & type_ ?~ SwaggerString
& default_ .~ decode "\"items\"")) & default_ .~ JSON.decode "\"items\""))
, ("offset", (mempty :: Param) , ("offset", (mempty :: Param)
& name .~ "offset" & name .~ "offset"
& description ?~ "Limiting and Pagination" & description ?~ "Limiting and Pagination"
@@ -303,9 +314,6 @@ postgrestSpec pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
h' = Just $ Host (unpack $ escapeHostName h) (Just (fromInteger p)) h' = Just $ Host (unpack $ escapeHostName h) (Just (fromInteger p))
d = fromMaybe "This is a dynamic API generated by PostgREST" sd d = fromMaybe "This is a dynamic API generated by PostgREST" sd
encodeOpenAPI :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> LByteString
encodeOpenAPI pds ti uri sd pks = encode $ postgrestSpec pds ti uri sd pks
pickProxy :: Maybe Text -> Maybe Proxy pickProxy :: Maybe Text -> Maybe Proxy
pickProxy proxy pickProxy proxy
| isNothing proxy = Nothing | isNothing proxy = Nothing
@@ -334,3 +342,18 @@ pickProxy proxy
("", "http") -> 80 ("", "http") -> 80
("", "https") -> 443 ("", "https") -> 443
_ -> readPort $ unpack $ tail $ pack port' _ -> readPort $ unpack $ tail $ pack port'
proxyUri :: AppConfig -> (Text, Text, Integer, Text)
proxyUri AppConfig{..} =
case pickProxy $ toS <$> configOpenApiServerProxyUri of
Just Proxy{..} ->
(proxyScheme, proxyHost, proxyPort, proxyPath)
Nothing ->
("http", configServerHost, toInteger configServerPort, "/")
openApiTableInfo :: DbStructure -> Table -> (Table, [Column], [Text])
openApiTableInfo dbStructure table =
( table
, tableCols dbStructure (tableSchema table) (tableName table)
, tablePKCols dbStructure (tableSchema table) (tableName table)
)
+4 -4
View File
@@ -45,7 +45,7 @@ import Text.InterpolatedString.Perl6 (q)
is represented as a list of strings containing variable bindings like is represented as a list of strings containing variable bindings like
@"k1=eq.42"@, or the empty list if there is no location header. @"k1=eq.42"@, or the empty list if there is no location header.
-} -}
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either SimpleError [GucHeader], Either SimpleError (Maybe Status)) type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status))
createWriteStatement :: H.Snippet -> H.Snippet -> Bool -> Bool -> Bool -> createWriteStatement :: H.Snippet -> H.Snippet -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] -> PgVersion -> Bool -> PreferRepresentation -> [Text] -> PgVersion -> Bool ->
@@ -130,7 +130,7 @@ standardRow = (,,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
<*> (fromMaybe (Right []) <$> nullableColumn decodeGucHeaders) <*> (fromMaybe (Right []) <$> nullableColumn decodeGucHeaders)
<*> (fromMaybe (Right Nothing) <$> nullableColumn decodeGucStatus) <*> (fromMaybe (Right Nothing) <$> nullableColumn decodeGucStatus)
type ProcResults = (Maybe Int64, Int64, ByteString, Either SimpleError [GucHeader], Either SimpleError (Maybe Status)) type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status))
callProcStatement :: Bool -> Bool -> H.Snippet -> H.Snippet -> H.Snippet -> Bool -> callProcStatement :: Bool -> Bool -> H.Snippet -> H.Snippet -> H.Snippet -> Bool ->
Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion -> Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion -> Bool ->
@@ -189,10 +189,10 @@ createExplainStatement countQuery =
let row = HD.singleRow $ column HD.bytea in let row = HD.singleRow $ column HD.bytea in
(^? L.nth 0 . L.key "Plan" . L.key "Plan Rows" . L._Integral) <$> row (^? L.nth 0 . L.key "Plan" . L.key "Plan Rows" . L._Integral) <$> row
decodeGucHeaders :: HD.Value (Either SimpleError [GucHeader]) decodeGucHeaders :: HD.Value (Either Error [GucHeader])
decodeGucHeaders = first (const GucHeadersError) . JSON.eitherDecode . toS <$> HD.bytea decodeGucHeaders = first (const GucHeadersError) . JSON.eitherDecode . toS <$> HD.bytea
decodeGucStatus :: HD.Value (Either SimpleError (Maybe Status)) decodeGucStatus :: HD.Value (Either Error (Maybe Status))
decodeGucStatus = first (const GucStatusError) . fmap (Just . toEnum . fst) . decimal <$> HD.text decodeGucStatus = first (const GucStatusError) . fmap (Just . toEnum . fst) . decimal <$> HD.text
-- | Get db settings from the connection role. Only used for configuration. -- | Get db settings from the connection role. Only used for configuration.
+1 -2
View File
@@ -22,8 +22,7 @@ import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Text.Heredoc import Text.Heredoc
import PostgREST.Auth (parseSecret) import PostgREST.Config (AppConfig (..), parseSecret)
import PostgREST.Config (AppConfig (..))
import PostgREST.Types (JSPathExp (..), LogLevel (..)) import PostgREST.Types (JSPathExp (..), LogLevel (..))
import Protolude hiding (toS) import Protolude hiding (toS)
import Protolude.Conv (toS) import Protolude.Conv (toS)
+2
View File
@@ -297,6 +297,7 @@ def test_expected_config_from_db_settings(defaultenv, role, expectedconfig):
assert dumpconfig(configpath=config, env=env) == expected assert dumpconfig(configpath=config, env=env) == expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config", "config",
[conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".config"], [conf for conf in CONFIGSDIR.iterdir() if conf.suffix == ".config"],
@@ -604,6 +605,7 @@ def test_max_rows_notify_reload(defaultenv):
# reset max-rows config on the db # reset max-rows config on the db
postgrest.session.post("/rpc/reset_max_rows_config") postgrest.session.post("/rpc/reset_max_rows_config")
def test_invalid_role_claim_key_notify_reload(defaultenv): def test_invalid_role_claim_key_notify_reload(defaultenv):
"NOTIFY reload config should show an error if role-claim-key is invalid" "NOTIFY reload config should show an error if role-claim-key is invalid"