refactor: simplify control flow in App.postgrest
Currently, authentication and response execution each unwrap ExceptT with separate runExceptT calls, which split the main request flow across nested pattern matching and Either handling. Control flow is complex and difficult to understand. The goal of this change is to make request execution as sequential monadic code with clear error handling. To implement that, request handling is now run in ExceptT over WriterT (Last ByteString) IO monad stack. Auth role is written after authentication succeeds and further returned along the response. Thanks to it response observation generation is centralized at the end of request handling. It was necessary to abstract monad stack in getAuthResult, lookupJwtCache, postgrestResponse, and withTiming to enable introduction of WriterT.
This commit is contained in:
committed by
Taimoor Zaeem
parent
1d6e0bd35f
commit
13c0e7061e
+21
-23
@@ -9,6 +9,7 @@ Some of its functionality includes:
|
|||||||
- Producing HTTP Headers according to RFCs.
|
- Producing HTTP Headers according to RFCs.
|
||||||
- Content Negotiation
|
- Content Negotiation
|
||||||
-}
|
-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
@@ -61,6 +62,7 @@ import PostgREST.SchemaCache (SchemaCache (..))
|
|||||||
import PostgREST.TimeIt (timeItT)
|
import PostgREST.TimeIt (timeItT)
|
||||||
import PostgREST.Version (docsVersion, prettyVersion)
|
import PostgREST.Version (docsVersion, prettyVersion)
|
||||||
|
|
||||||
|
import Control.Monad.Writer
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import Data.Streaming.Network (bindPortTCP)
|
import Data.Streaming.Network (bindPortTCP)
|
||||||
@@ -127,24 +129,19 @@ postgrest appState connWorker =
|
|||||||
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
|
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
|
||||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||||
|
|
||||||
let observer = AppState.getObserver appState
|
let handleError = fmap (either (Error.errorResponseFor configClientErrorVerbosity) identity)
|
||||||
bearerAuth = ApiRequest.userBearerAuth req
|
|
||||||
|
|
||||||
response <- do
|
-- writer to save authRole (uses `tell` for this and `getLast` to obtain it)
|
||||||
authResultE <- runExceptT $ withTiming appConf $
|
-- has to be before runExceptT to make sure role is not lost on error
|
||||||
liftIO (Auth.getAuthResult appState bearerAuth) >>= liftEither
|
(response, authRole) <- runWriterT . handleError . runExceptT $ do
|
||||||
|
(jwtTime, authResult@AuthResult{..}) <- withTiming appConf $
|
||||||
|
Auth.getAuthResult appState $ ApiRequest.userBearerAuth req
|
||||||
|
|
||||||
case authResultE of
|
tell $ pure authRole
|
||||||
Left err -> do
|
|
||||||
let resp = Error.errorResponseFor configClientErrorVerbosity err
|
|
||||||
observer $ genResponseObs Nothing req resp
|
|
||||||
pure resp
|
|
||||||
|
|
||||||
Right (jwtTime, authResult@AuthResult{..}) -> do
|
postgrestResponse appState appConf maybeSchemaCache jwtTime authResult req
|
||||||
resp <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$>
|
|
||||||
runExceptT (postgrestResponse appState appConf maybeSchemaCache jwtTime authResult req)
|
AppState.getObserver appState $ genResponseObs (getLast authRole) req response
|
||||||
observer $ genResponseObs (Just authRole) req resp
|
|
||||||
pure resp
|
|
||||||
|
|
||||||
-- Launch the connWorker when the connection is down. The postgrest
|
-- Launch the connWorker when the connection is down. The postgrest
|
||||||
-- function can respond successfully (with a stale schema cache) before
|
-- function can respond successfully (with a stale schema cache) before
|
||||||
@@ -164,13 +161,14 @@ postgrest appState connWorker =
|
|||||||
ResponseObs user req (Wai.responseStatus resp) (WaiHeader.contentLength $ Wai.responseHeaders resp)
|
ResponseObs user req (Wai.responseStatus resp) (WaiHeader.contentLength $ Wai.responseHeaders resp)
|
||||||
|
|
||||||
postgrestResponse
|
postgrestResponse
|
||||||
:: AppState.AppState
|
:: (MonadError Error m, MonadIO m)
|
||||||
|
=> AppState.AppState
|
||||||
-> AppConfig
|
-> AppConfig
|
||||||
-> Maybe SchemaCache
|
-> Maybe SchemaCache
|
||||||
-> Maybe Double
|
-> Maybe Double
|
||||||
-> AuthResult
|
-> AuthResult
|
||||||
-> Wai.Request
|
-> Wai.Request
|
||||||
-> ExceptT Error IO Wai.Response
|
-> m Wai.Response
|
||||||
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResult@AuthResult{..} req = do
|
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResult@AuthResult{..} req = do
|
||||||
let observer = AppState.getObserver appState
|
let observer = AppState.getObserver appState
|
||||||
|
|
||||||
@@ -179,12 +177,12 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
|||||||
Just sCache ->
|
Just sCache ->
|
||||||
return sCache
|
return sCache
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
lift $ observer SchemaCacheEmptyObs
|
liftIO $ observer SchemaCacheEmptyObs
|
||||||
throwError Error.NoSchemaCacheError
|
throwError Error.NoSchemaCacheError
|
||||||
|
|
||||||
let prefs = ApiRequest.userPreferences conf req (dbTimezones sCache)
|
let prefs = ApiRequest.userPreferences conf req (dbTimezones sCache)
|
||||||
|
|
||||||
body <- lift $ 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
|
||||||
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||||
@@ -197,13 +195,13 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
|||||||
case tx of
|
case tx of
|
||||||
MainTx.NoDbTx r -> pure r
|
MainTx.NoDbTx r -> pure r
|
||||||
MainTx.DbTx dbSession -> do
|
MainTx.DbTx dbSession -> do
|
||||||
dbRes <- lift $ AppState.usePool appState dbSession
|
dbRes <- liftIO $ AppState.usePool appState dbSession
|
||||||
let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes
|
let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes
|
||||||
|
|
||||||
-- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message.
|
-- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message.
|
||||||
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
|
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
|
||||||
-- This needs refactoring so only the below obsQuery is used.
|
-- This needs refactoring so only the below obsQuery is used.
|
||||||
lift $ whenLeft eitherResp $ obsQuery . Error.status
|
liftIO $ whenLeft eitherResp $ obsQuery . Error.status
|
||||||
liftEither eitherResp
|
liftEither eitherResp
|
||||||
|
|
||||||
(respTime, resp) <- withTiming conf $ do
|
(respTime, resp) <- withTiming conf $ do
|
||||||
@@ -211,7 +209,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
|||||||
status' = either Error.status Response.pgrstStatus response
|
status' = either Error.status Response.pgrstStatus response
|
||||||
|
|
||||||
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
|
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
|
||||||
lift $ obsQuery status'
|
liftIO $ obsQuery status'
|
||||||
liftEither response
|
liftEither response
|
||||||
|
|
||||||
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp
|
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp
|
||||||
@@ -230,7 +228,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
|
|||||||
varyHeaderPresent :: [HTTP.Header] -> Bool
|
varyHeaderPresent :: [HTTP.Header] -> Bool
|
||||||
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
|
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
|
||||||
|
|
||||||
withTiming :: AppConfig -> ExceptT e IO a -> ExceptT e IO (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
|
||||||
(t, r) <- timeItT f
|
(t, r) <- timeItT f
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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.
|
||||||
-}
|
-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
module PostgREST.Auth
|
module PostgREST.Auth
|
||||||
( getAuthResult )
|
( getAuthResult )
|
||||||
where
|
where
|
||||||
@@ -25,14 +26,9 @@ import Protolude
|
|||||||
|
|
||||||
-- | Perform authentication and authorization
|
-- | Perform authentication and authorization
|
||||||
-- Parse JWT and return AuthResult
|
-- Parse JWT and return AuthResult
|
||||||
getAuthResult :: AppState -> Maybe ByteString -> IO (Either Error AuthResult)
|
getAuthResult :: (MonadError Error m, MonadIO m) => AppState -> Maybe ByteString -> m AuthResult
|
||||||
getAuthResult appState token = do
|
getAuthResult appState token = do
|
||||||
conf <- getConfig appState
|
conf <- liftIO $ getConfig appState
|
||||||
time <- getTime appState
|
time <- liftIO $ getTime appState
|
||||||
|
|
||||||
let jwtCacheState = getJwtCacheState appState
|
parseClaims conf time =<< lookupJwtCache (getJwtCacheState appState) token
|
||||||
parseJwt = runExceptT $ do
|
|
||||||
claims <- lookupJwtCache jwtCacheState token
|
|
||||||
parseClaims conf time claims
|
|
||||||
|
|
||||||
parseJwt
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Description : PostgREST JWT validation results Cache.
|
|||||||
This module provides functions to deal with the JWT cache.
|
This module provides functions to deal with the JWT cache.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE ExistentialQuantification #-}
|
{-# LANGUAGE ExistentialQuantification #-}
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
@@ -42,7 +43,7 @@ import Protolude
|
|||||||
data JwtCacheState = JwtCacheState ObservationHandler (IORef JwtCache)
|
data JwtCacheState = JwtCacheState ObservationHandler (IORef JwtCache)
|
||||||
|
|
||||||
class CacheVariant m v where
|
class CacheVariant m v where
|
||||||
cached :: SC.Cache m ByteString v -> ByteString -> ExceptT Error IO JSON.Object
|
cached :: (MonadError Error n, MonadIO n) => SC.Cache m ByteString v -> ByteString -> n JSON.Object
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Jwt caching can have three different configurations:
|
Jwt caching can have three different configurations:
|
||||||
@@ -60,12 +61,12 @@ data JwtCache =
|
|||||||
forall m v. CacheVariant m v => JwtCache JwkSet (TVar Int) (SC.Cache m ByteString v)
|
forall m v. CacheVariant m v => JwtCache JwkSet (TVar Int) (SC.Cache m ByteString v)
|
||||||
|
|
||||||
instance CacheVariant IO (Either Error JSON.Object) where
|
instance CacheVariant IO (Either Error JSON.Object) where
|
||||||
cached c = lift . SC.cached c >=> liftEither
|
cached c = liftIO . SC.cached c >=> liftEither
|
||||||
|
|
||||||
instance CacheVariant (ExceptT Error IO) JSON.Object where
|
instance CacheVariant (ExceptT Error IO) JSON.Object where
|
||||||
cached = SC.cached
|
cached c = liftIO . runExceptT . SC.cached c >=> liftEither
|
||||||
|
|
||||||
decode :: JwtCache -> ByteString -> ExceptT Error IO JSON.Object
|
decode :: (MonadError Error m, MonadIO m) => JwtCache -> ByteString -> m JSON.Object
|
||||||
decode JwtNoJwks = const $ throwError (JwtErr JwtSecretMissing)
|
decode JwtNoJwks = const $ throwError (JwtErr JwtSecretMissing)
|
||||||
decode (JwtNoCache key) = parseAndDecodeClaims key
|
decode (JwtNoCache key) = parseAndDecodeClaims key
|
||||||
decode (JwtCache _ _ c) = cached c
|
decode (JwtCache _ _ c) = cached c
|
||||||
@@ -110,5 +111,5 @@ newJwtCache AppConfig{configJWKS, configJwtCacheMaxEntries} observationHandler =
|
|||||||
(const . const $ lift $ observationHandler JwtCacheEviction) -- evictions metrics
|
(const . const $ lift $ observationHandler JwtCacheEviction) -- evictions metrics
|
||||||
alwaysValid) -- no invalidation for now
|
alwaysValid) -- no invalidation for now
|
||||||
|
|
||||||
lookupJwtCache :: JwtCacheState -> Maybe ByteString -> ExceptT Error IO JSON.Object
|
lookupJwtCache :: (MonadError Error m, MonadIO m) => JwtCacheState -> Maybe ByteString -> m JSON.Object
|
||||||
lookupJwtCache (JwtCacheState _ cacheState) k = liftIO (readIORef cacheState) >>= flip (maybe (pure KM.empty)) k . decode
|
lookupJwtCache (JwtCacheState _ cacheState) k = liftIO (readIORef cacheState) >>= flip (maybe (pure KM.empty)) k . decode
|
||||||
|
|||||||
Reference in New Issue
Block a user