Skip to content

Commit e206b59

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 0df56f9 commit e206b59

3 files changed

Lines changed: 36 additions & 37 deletions

File tree

src/PostgREST/App.hs

Lines changed: 25 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,23 @@ 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+
let handleErrors = fmap (either (Error.errorResponseFor configClientErrorVerbosity) identity)
132133

133-
response <- do
134-
authResultE <- runExceptT $ withTiming appConf $
135-
liftIO (Auth.getAuthResult appState bearerAuth) >>= liftEither
134+
(response, authRole) <-
135+
-- writer to save authRole
136+
-- has to be before runExceptT to make sure role is not lost on error
137+
runWriterT $
138+
handleErrors $
139+
runExceptT $
140+
do
141+
(jwtTime, authResult@AuthResult{..}) <- withTiming appConf $
142+
Auth.getAuthResult appState $ ApiRequest.userBearerAuth req
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+
tell $ pure authRole
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+
postgrestResponse appState appConf maybeSchemaCache jwtTime authResult req
147+
148+
AppState.getObserver appState $ genResponseObs (getLast authRole) req response
148149

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

166167
postgrestResponse
167-
:: AppState.AppState
168+
:: (MonadError Error m, MonadIO m)
169+
=> AppState.AppState
168170
-> AppConfig
169171
-> Maybe SchemaCache
170172
-> Maybe Double
171173
-> AuthResult
172174
-> Wai.Request
173-
-> ExceptT Error IO Wai.Response
175+
-> m Wai.Response
174176
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResult@AuthResult{..} req = do
175177
let observer = AppState.getObserver appState
176178

@@ -179,12 +181,12 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
179181
Just sCache ->
180182
return sCache
181183
Nothing -> do
182-
lift $ observer SchemaCacheEmptyObs
184+
liftIO $ observer SchemaCacheEmptyObs
183185
throwError Error.NoSchemaCacheError
184186

185187
let prefs = ApiRequest.userPreferences conf req (dbTimezones sCache)
186188

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

189191
(parseTime, apiReq@ApiRequest{..}) <- withTiming conf $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
190192
(planTime, plan) <- withTiming conf $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
@@ -197,21 +199,21 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jwtTime authResul
197199
case tx of
198200
MainTx.NoDbTx r -> pure r
199201
MainTx.DbTx dbSession -> do
200-
dbRes <- lift $ AppState.usePool appState dbSession
202+
dbRes <- liftIO $ AppState.usePool appState dbSession
201203
let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes
202204

203205
-- 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.
204206
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
205207
-- This needs refactoring so only the below obsQuery is used.
206-
lift $ whenLeft eitherResp $ obsQuery . Error.status
208+
liftIO $ whenLeft eitherResp $ obsQuery . Error.status
207209
liftEither eitherResp
208210

209211
(respTime, resp) <- withTiming conf $ do
210212
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
211213
status' = either Error.status Response.pgrstStatus response
212214

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

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

233-
withTiming :: AppConfig -> ExceptT e IO a -> ExceptT e IO (Maybe Double, a)
235+
withTiming :: (MonadError e m, MonadIO m) => AppConfig -> m a -> m (Maybe Double, a)
234236
withTiming AppConfig{configServerTimingEnabled} f = if configServerTimingEnabled
235237
then do
236238
(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)