Skip to content

Commit 9d3be61

Browse files
committed
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.
1 parent 9162cea commit 9d3be61

3 files changed

Lines changed: 34 additions & 37 deletions

File tree

src/PostgREST/App.hs

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Some of its functionality includes:
99
- Producing HTTP Headers according to RFCs.
1010
- Content Negotiation
1111
-}
12+
{-# LANGUAGE FlexibleContexts #-}
1213
{-# LANGUAGE NamedFieldPuns #-}
1314
{-# LANGUAGE RecordWildCards #-}
1415
{-# LANGUAGE ScopedTypeVariables #-}
@@ -61,6 +62,7 @@ import PostgREST.SchemaCache (SchemaCache (..))
6162
import PostgREST.TimeIt (timeItT)
6263
import PostgREST.Version (docsVersion, prettyVersion)
6364

65+
import Control.Monad.Writer
6466
import qualified Data.ByteString.Char8 as BS
6567
import qualified Data.List as L
6668
import Data.Streaming.Network (bindPortTCP)
@@ -127,24 +129,21 @@ postgrest appState connWorker =
127129
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
128130
maybeSchemaCache <- AppState.getSchemaCache appState
129131

130-
let observer = AppState.getObserver appState
131-
bearerAuth = ApiRequest.userBearerAuth req
132+
(response, user) <-
133+
-- has to be before runExceptT to make sure role is not lost on error
134+
runWriterT $
135+
-- handle errors
136+
fmap (either (Error.errorResponseFor configClientErrorVerbosity) identity) $
137+
runExceptT $
138+
do
139+
(jwtTime, authResult@AuthResult{..}) <- withTiming appConf $
140+
Auth.getAuthResult appState $ ApiRequest.userBearerAuth req
132141

133-
response <- do
134-
authResultE <- runExceptT $ withTiming appConf $
135-
liftIO (Auth.getAuthResult appState bearerAuth) >>= liftEither
142+
tell $ pure authRole
136143

137-
case authResultE of
138-
Left err -> do
139-
let resp = Error.errorResponseFor configClientErrorVerbosity err
140-
observer $ genResponseObs Nothing req resp
141-
pure resp
144+
postgrestResponse appState appConf maybeSchemaCache jwtTime authResult req
142145

143-
Right (jwtTime, authResult@AuthResult{..}) -> do
144-
resp <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$>
145-
runExceptT (postgrestResponse appState appConf maybeSchemaCache jwtTime authResult req)
146-
observer $ genResponseObs (Just authRole) req resp
147-
pure resp
146+
AppState.getObserver appState $ genResponseObs (getLast user) req response
148147

149148
-- Launch the connWorker when the connection is down. The postgrest
150149
-- function can respond successfully (with a stale schema cache) before
@@ -164,13 +163,14 @@ postgrest appState connWorker =
164163
ResponseObs user req (Wai.responseStatus resp) (WaiHeader.contentLength $ Wai.responseHeaders resp)
165164

166165
postgrestResponse
167-
:: AppState.AppState
166+
:: (MonadError Error m, MonadIO m)
167+
=> AppState.AppState
168168
-> AppConfig
169169
-> Maybe SchemaCache
170170
-> Maybe Double
171171
-> AuthResult
172172
-> Wai.Request
173-
-> ExceptT Error IO Wai.Response
173+
-> m Wai.Response
174174
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResult@AuthResult{..} req = do
175175
let observer = AppState.getObserver appState
176176

@@ -179,12 +179,12 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
179179
Just sCache ->
180180
return sCache
181181
Nothing -> do
182-
lift $ observer SchemaCacheEmptyObs
182+
liftIO $ observer SchemaCacheEmptyObs
183183
throwError Error.NoSchemaCacheError
184184

185185
let prefs = ApiRequest.userPreferences conf req (dbTimezones sCache)
186186

187-
body <- lift $ Wai.strictRequestBody req
187+
body <- liftIO $ Wai.strictRequestBody req
188188

189189
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
190190
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
@@ -197,21 +197,21 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
197197
case tx of
198198
MainTx.NoDbTx r -> pure r
199199
MainTx.DbTx{..} -> do
200-
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
200+
dbRes <- liftIO $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
201201
let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes
202202

203203
-- 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.
204204
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
205205
-- This needs refactoring so only the below obsQuery is used.
206-
lift $ whenLeft eitherResp $ obsQuery . Error.status
206+
liftIO $ whenLeft eitherResp $ obsQuery . Error.status
207207
liftEither eitherResp
208208

209209
(respTime, resp) <- withTiming conf $ do
210210
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
211211
status' = either Error.status Response.pgrstStatus response
212212

213213
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
214-
lift $ obsQuery status'
214+
liftIO $ obsQuery status'
215215
liftEither response
216216

217217
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp
@@ -230,7 +230,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
230230
varyHeaderPresent :: [HTTP.Header] -> Bool
231231
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
232232

233-
withTiming :: AppConfig -> ExceptT e IO a -> ExceptT e IO (Maybe Double, a)
233+
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
234234
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
235235
then do
236236
(t, r) <- timeItT f

src/PostgREST/Auth.hs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Authentication should always be implemented in an external service.
1010
In the test suite there is an example of simple login function that can be used for a
1111
very simple authentication system inside the PostgreSQL database.
1212
-}
13+
{-# LANGUAGE FlexibleContexts #-}
1314
module PostgREST.Auth
1415
( getAuthResult )
1516
where
@@ -25,14 +26,9 @@ import Protolude
2526

2627
-- | Perform authentication and authorization
2728
-- Parse JWT and return AuthResult
28-
getAuthResult :: AppState -> Maybe ByteString -> IO (Either Error AuthResult)
29+
getAuthResult :: (MonadError Error m, MonadIO m) => AppState -> Maybe ByteString -> m AuthResult
2930
getAuthResult appState token = do
30-
conf <- getConfig appState
31-
time <- getTime appState
31+
conf <- liftIO $ getConfig appState
32+
time <- liftIO $ getTime appState
3233

33-
let jwtCacheState = getJwtCacheState appState
34-
parseJwt = runExceptT $ do
35-
claims <- lookupJwtCache jwtCacheState token
36-
parseClaims conf time claims
37-
38-
parseJwt
34+
parseClaims conf time =<< lookupJwtCache (getJwtCacheState appState) token

src/PostgREST/Auth/JwtCache.hs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Description : PostgREST JWT validation results Cache.
55
This module provides functions to deal with the JWT cache.
66
-}
77
{-# LANGUAGE ExistentialQuantification #-}
8+
{-# LANGUAGE FlexibleContexts #-}
89
{-# LANGUAGE FlexibleInstances #-}
910
{-# LANGUAGE LambdaCase #-}
1011
{-# LANGUAGE MultiParamTypeClasses #-}
@@ -42,7 +43,7 @@ import Protolude
4243
data JwtCacheState = JwtCacheState ObservationHandler (IORef JwtCache)
4344

4445
class CacheVariant m v where
45-
cached :: SC.Cache m ByteString v -> ByteString -> ExceptT Error IO JSON.Object
46+
cached :: (MonadError Error n, MonadIO n) => SC.Cache m ByteString v -> ByteString -> n JSON.Object
4647

4748
{-|
4849
Jwt caching can have three different configurations:
@@ -60,12 +61,12 @@ data JwtCache =
6061
forall m v. CacheVariant m v => JwtCache JwkSet (TVar Int) (SC.Cache m ByteString v)
6162

6263
instance CacheVariant IO (Either Error JSON.Object) where
63-
cached c = lift . SC.cached c >=> liftEither
64+
cached c = liftIO . SC.cached c >=> liftEither
6465

6566
instance CacheVariant (ExceptT Error IO) JSON.Object where
66-
cached = SC.cached
67+
cached c = liftIO . runExceptT . SC.cached c >=> liftEither
6768

68-
decode :: JwtCache -> ByteString -> ExceptT Error IO JSON.Object
69+
decode :: (MonadError Error m, MonadIO m) => JwtCache -> ByteString -> m JSON.Object
6970
decode JwtNoJwks = const $ throwError (JwtErr JwtSecretMissing)
7071
decode (JwtNoCache key) = parseAndDecodeClaims key
7172
decode (JwtCache _ _ c) = cached c
@@ -110,5 +111,5 @@ newJwtCache AppConfig{configJWKS, configJwtCacheMaxEntries} observationHandler =
110111
(const . const $ lift $ observationHandler JwtCacheEviction) -- evictions metrics
111112
alwaysValid) -- no invalidation for now
112113

113-
lookupJwtCache :: JwtCacheState -> Maybe ByteString -> ExceptT Error IO JSON.Object
114+
lookupJwtCache :: (MonadError Error m, MonadIO m) => JwtCacheState -> Maybe ByteString -> m JSON.Object
114115
lookupJwtCache (JwtCacheState _ cacheState) k = liftIO (readIORef cacheState) >>= flip (maybe (pure KM.empty)) k . decode

0 commit comments

Comments
 (0)