refactor: config validation inside readAppConfig

Remove Either from configJwtRoleClaimKey/configServerUnixSocketMode
and remove whenLefts.
This commit is contained in:
steve-chavez
2021-01-22 15:56:08 -05:00
committed by Steve Chavez
parent 4344cc9202
commit 17af56adb1
8 changed files with 38 additions and 41 deletions
+5 -5
View File
@@ -16,12 +16,12 @@ import System.Posix.Types (FileMode)
import Protolude import Protolude
createAndBindSocket :: FilePath -> Maybe FileMode -> IO Socket createAndBindSocket :: FilePath -> FileMode -> IO Socket
createAndBindSocket socketFilePath maybeSocketFileMode = do createAndBindSocket socketFilePath socketFileMode = do
deleteSocketFileIfExist socketFilePath deleteSocketFileIfExist socketFilePath
sock <- socket AF_UNIX Stream defaultProtocol sock <- socket AF_UNIX Stream defaultProtocol
bind sock $ SockAddrUnix socketFilePath bind sock $ SockAddrUnix socketFilePath
mapM_ (setFileMode socketFilePath) maybeSocketFileMode setFileMode socketFilePath socketFileMode
return sock return sock
where where
deleteSocketFileIfExist path = removeFile path `catch` handleDoesNotExist deleteSocketFileIfExist path = removeFile path `catch` handleDoesNotExist
@@ -30,9 +30,9 @@ createAndBindSocket socketFilePath maybeSocketFileMode = do
| otherwise = throwIO e | otherwise = throwIO e
-- run the postgrest application with user defined socket. -- run the postgrest application with user defined socket.
runAppInSocket :: Settings -> Application -> Either Text FileMode -> FilePath -> IO () runAppInSocket :: Settings -> Application -> FileMode -> FilePath -> IO ()
runAppInSocket settings app socketFileMode sockPath = do runAppInSocket settings app socketFileMode sockPath = do
sock <- createAndBindSocket sockPath (rightToMaybe socketFileMode) sock <- createAndBindSocket sockPath socketFileMode
putStrLn $ ("Listening on unix socket " :: Text) <> show sockPath putStrLn $ ("Listening on unix socket " :: Text) <> show sockPath
listen sock maxListenQueue listen sock maxListenQueue
runSettingsSocket settings sock app runSettingsSocket settings sock app
+1 -1
View File
@@ -80,7 +80,7 @@ postgrest logLev refConf refDbStructure pool getTime connWorker =
Left err -> return . errorResponseFor $ err Left err -> return . errorResponseFor $ err
Right apiRequest -> do Right apiRequest -> do
-- The jwt must be checked before touching the db. -- The jwt must be checked before touching the db.
attempt <- attemptJwtClaims (configJWKS conf) (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configJwtRoleClaimKey conf) attempt <- attemptJwtClaims (configJWKS conf) (configJwtAudience conf) (toS $ iJWT apiRequest) time (configJwtRoleClaimKey conf)
case jwtClaims attempt of case jwtClaims attempt of
Left errJwt -> return . errorResponseFor $ errJwt Left errJwt -> return . errorResponseFor $ errJwt
Right claims -> do Right claims -> do
+3 -3
View File
@@ -54,7 +54,7 @@ jwtClaims attempt =
Receives the JWT secret and audience (from config) and a JWT and returns a map Receives the JWT secret and audience (from config) and a JWT and returns a map
of JWT claims. of JWT claims.
-} -}
attemptJwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt attemptJwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> JSPath -> IO JWTAttempt
attemptJwtClaims _ _ "" _ _ = return $ JWTClaims M.empty attemptJwtClaims _ _ "" _ _ = return $ JWTClaims M.empty
attemptJwtClaims maybeSecret audience payload time jspath = attemptJwtClaims maybeSecret audience payload time jspath =
case maybeSecret of case maybeSecret of
@@ -72,11 +72,11 @@ attemptJwtClaims maybeSecret audience payload time jspath =
Turn JWT ClaimSet into something easier to work with, Turn JWT ClaimSet into something easier to work with,
also here the jspath is applied to put the "role" in the map also here the jspath is applied to put the "role" in the map
-} -}
claims2map :: ClaimsSet -> Maybe JSPath -> M.HashMap Text JSON.Value claims2map :: ClaimsSet -> JSPath -> M.HashMap Text JSON.Value
claims2map claims jspath = (\case claims2map claims jspath = (\case
val@(JSON.Object o) -> val@(JSON.Object o) ->
let role = maybe M.empty (M.singleton "role") $ let role = maybe M.empty (M.singleton "role") $
walkJSPath (Just val) =<< jspath in walkJSPath (Just val) jspath in
M.delete "role" o `M.union` role -- mutating the map M.delete "role" o `M.union` role -- mutating the map
_ -> M.empty _ -> M.empty
) $ JSON.toJSON claims ) $ JSON.toJSON claims
+24 -25
View File
@@ -44,7 +44,6 @@ import Control.Lens (preview)
import Control.Monad (fail) import Control.Monad (fail)
import Crypto.JWT (JWKSet, StringOrURI, stringOrUri) import Crypto.JWT (JWKSet, StringOrURI, stringOrUri)
import Data.Aeson (encode, toJSON) import Data.Aeson (encode, toJSON)
import Data.Either.Combinators (fromRight', whenLeft)
import Data.List (lookup) import Data.List (lookup)
import Data.List.NonEmpty (fromList, toList) import Data.List.NonEmpty (fromList, toList)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
@@ -108,7 +107,7 @@ data AppConfig = AppConfig {
, configDbUri :: Text , configDbUri :: Text
, configJWKS :: Maybe JWKSet , configJWKS :: Maybe JWKSet
, configJwtAudience :: Maybe StringOrURI , configJwtAudience :: Maybe StringOrURI
, configJwtRoleClaimKey :: Either Text JSPath , configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe B.ByteString , configJwtSecret :: Maybe B.ByteString
, configJwtSecretIsBase64 :: Bool , configJwtSecretIsBase64 :: Bool
, configLogLevel :: LogLevel , configLogLevel :: LogLevel
@@ -117,7 +116,7 @@ data AppConfig = AppConfig {
, configServerHost :: Text , configServerHost :: Text
, configServerPort :: Int , configServerPort :: Int
, configServerUnixSocket :: Maybe FilePath , configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: Either Text FileMode , configServerUnixSocketMode :: FileMode
} }
configDbPoolTimeout' :: (Fractional a) => AppConfig -> a configDbPoolTimeout' :: (Fractional a) => AppConfig -> a
@@ -289,7 +288,7 @@ dumpAppConfig conf =
,("db-tx-end", q . showTxEnd) ,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri) ,("db-uri", q . configDbUri)
,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience) ,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience)
,("jwt-role-claim-key", q . intercalate mempty . fmap show . fromRight' . configJwtRoleClaimKey) ,("jwt-role-claim-key", q . intercalate mempty . fmap show . configJwtRoleClaimKey)
,("jwt-secret", q . toS . showJwtSecret) ,("jwt-secret", q . toS . showJwtSecret)
,("jwt-secret-is-base64", toLower . show . configJwtSecretIsBase64) ,("jwt-secret-is-base64", toLower . show . configJwtSecretIsBase64)
,("log-level", q . show . configLogLevel) ,("log-level", q . show . configLogLevel)
@@ -317,7 +316,7 @@ dumpAppConfig conf =
| otherwise = toS secret | otherwise = toS secret
where where
secret = fromMaybe mempty $ configJwtSecret c secret = fromMaybe mempty $ configJwtSecret c
showSocketMode c = showOct (fromRight' $ configServerUnixSocketMode c) "" showSocketMode c = showOct (configServerUnixSocketMode c) mempty
-- This class is needed for the polymorphism of overrideFromDbOrEnvironment -- This class is needed for the polymorphism of overrideFromDbOrEnvironment
-- because C.required and C.optional have different signatures -- because C.required and C.optional have different signatures
@@ -374,13 +373,12 @@ readAppConfig dbSettings env optPath = do
<*> reqString "db-uri" <*> reqString "db-uri"
<*> pure Nothing <*> pure Nothing
<*> parseJwtAudience "jwt-aud" <*> parseJwtAudience "jwt-aud"
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> optWithAlias (optValue "jwt-role-claim-key") <*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
(optValue "role-claim-key"))
<*> (fmap encodeUtf8 <$> optString "jwt-secret") <*> (fmap encodeUtf8 <$> optString "jwt-secret")
<*> (fromMaybe False <$> optWithAlias (optBool "jwt-secret-is-base64") <*> (fromMaybe False <$> optWithAlias (optBool "jwt-secret-is-base64")
(optBool "secret-is-base64")) (optBool "secret-is-base64"))
<*> parseLogLevel "log-level" <*> parseLogLevel "log-level"
<*> optString "openapi-server-proxy-uri" <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types") <*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
<*> (fromMaybe "!4" <$> optString "server-host") <*> (fromMaybe "!4" <$> optString "server-host")
<*> (fromMaybe 3000 <$> optInt "server-port") <*> (fromMaybe 3000 <$> optInt "server-port")
@@ -394,18 +392,25 @@ readAppConfig dbSettings env optPath = do
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
normalize k = ("app.settings." <>) <$> stripPrefix "PGRST_APP_SETTINGS_" (toS k) normalize k = ("app.settings." <>) <$> stripPrefix "PGRST_APP_SETTINGS_" (toS k)
parseSocketFileMode :: C.Key -> C.Parser C.Config (Either Text FileMode) parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
parseSocketFileMode k = parseSocketFileMode k =
optString k >>= \case optString k >>= \case
Nothing -> pure $ Right 432 -- return default 660 mode if no value was provided Nothing -> pure $ 432 -- return default 660 mode if no value was provided
Just fileModeText -> Just fileModeText ->
case (readOct . unpack) fileModeText of case (readOct . unpack) fileModeText of
[] -> [] ->
pure $ Left "Invalid server-unix-socket-mode: not an octal" fail "Invalid server-unix-socket-mode: not an octal"
(fileMode, _):_ -> (fileMode, _):_ ->
if fileMode < 384 || fileMode > 511 if fileMode < 384 || fileMode > 511
then pure $ Left "Invalid server-unix-socket-mode: needs to be between 600 and 777" then fail "Invalid server-unix-socket-mode: needs to be between 600 and 777"
else pure $ Right fileMode else pure fileMode
parseOpenAPIServerProxyURI :: C.Key -> C.Parser C.Config (Maybe Text)
parseOpenAPIServerProxyURI k =
optString k >>= \case
Nothing -> pure Nothing
Just val | isMalformedProxyUri val -> fail "Malformed proxy uri, a correct example: https://example.com:8443/basePath"
| otherwise -> pure $ Just val
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI) parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
parseJwtAudience k = parseJwtAudience k =
@@ -436,6 +441,12 @@ readAppConfig dbSettings env optPath = do
Just "rollback-allow-override" -> pure $ f (True, True) Just "rollback-allow-override" -> pure $ f (True, True)
Just _ -> fail "Invalid transaction termination. Check your configuration." Just _ -> fail "Invalid transaction termination. Check your configuration."
parseRoleClaimKey :: C.Key -> C.Key -> C.Parser C.Config JSPath
parseRoleClaimKey k al =
optWithAlias (optString k) (optString al) >>= \case
Nothing -> pure [JSPKey "role"]
Just rck -> either (fail . show) pure $ pRoleClaimKey rck
reqWithAlias :: C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a) -> [Char] -> C.Parser C.Config a reqWithAlias :: C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a) -> [Char] -> C.Parser C.Config a
reqWithAlias orig alias err = reqWithAlias orig alias err =
orig >>= \case orig >>= \case
@@ -503,10 +514,6 @@ readAppConfig dbSettings env optPath = do
Nothing -> (> 0) <$> (readMaybe $ toS s :: Maybe Integer) Nothing -> (> 0) <$> (readMaybe $ toS s :: Maybe Integer)
coerceBool _ = Nothing coerceBool _ = Nothing
parseRoleClaimKey :: C.Value -> Either Text JSPath
parseRoleClaimKey (C.String s) = pRoleClaimKey s
parseRoleClaimKey v = pRoleClaimKey $ show v
splitOnCommas :: C.Value -> [Text] splitOnCommas :: C.Value -> [Text]
splitOnCommas (C.String s) = strip <$> splitOn "," s splitOnCommas (C.String s) = strip <$> splitOn "," s
splitOnCommas _ = [] splitOnCommas _ = []
@@ -520,14 +527,6 @@ readAppConfig dbSettings env optPath = do
readValidateConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig readValidateConfig :: [(Text, Text)] -> Environment -> Maybe FilePath -> IO AppConfig
readValidateConfig dbSettings env path = do readValidateConfig dbSettings env path = do
conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig dbSettings env path conf <- loadDbUriFile =<< loadSecretFile =<< readAppConfig dbSettings env path
-- Checks that the provided proxy uri is formated correctly
when (isMalformedProxyUri $ toS <$> configOpenApiServerProxyUri conf) $
panic
"Malformed proxy uri, a correct example: https://example.com:8443/basePath"
-- Checks that the provided jspath is valid
whenLeft (configJwtRoleClaimKey conf) panic
-- Check the file mode is valid
whenLeft (configServerUnixSocketMode conf) panic
return $ conf { configJWKS = parseSecret <$> configJwtSecret conf} return $ conf { configJWKS = parseSecret <$> configJwtSecret conf}
type Environment = M.Map [Char] Text type Environment = M.Map [Char] Text
+1 -1
View File
@@ -312,7 +312,7 @@ pickProxy proxy
-- should never happen -- should never happen
-- since the request would have been rejected by the middleware if proxy uri -- since the request would have been rejected by the middleware if proxy uri
-- is malformed -- is malformed
| isMalformedProxyUri proxy = Nothing | isMalformedProxyUri $ fromMaybe mempty proxy = Nothing
| otherwise = Just Proxy { | otherwise = Just Proxy {
proxyScheme = scheme proxyScheme = scheme
, proxyHost = host' , proxyHost = host'
+2 -3
View File
@@ -25,9 +25,8 @@ import Protolude.Conv (toS)
http://postgrest.com/openapi.json http://postgrest.com/openapi.json
https://postgrest.com:8080/openapi.json https://postgrest.com:8080/openapi.json
-} -}
isMalformedProxyUri :: Maybe Text -> Bool isMalformedProxyUri :: Text -> Bool
isMalformedProxyUri Nothing = False isMalformedProxyUri uri
isMalformedProxyUri (Just uri)
| isAbsoluteURI (toS uri) = not $ isUriValid $ toURI uri | isAbsoluteURI (toS uri) = not $ isUriValid $ toURI uri
| otherwise = True | otherwise = True
+2 -2
View File
@@ -82,7 +82,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configDbUri = mempty , configDbUri = mempty
, configJWKS = parseSecret <$> secret , configJWKS = parseSecret <$> secret
, configJwtAudience = Nothing , configJwtAudience = Nothing
, configJwtRoleClaimKey = Right [JSPKey "role"] , configJwtRoleClaimKey = [JSPKey "role"]
, configJwtSecret = secret , configJwtSecret = secret
, configJwtSecretIsBase64 = False , configJwtSecretIsBase64 = False
, configLogLevel = LogCrit , configLogLevel = LogCrit
@@ -91,7 +91,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
, configServerHost = "localhost" , configServerHost = "localhost"
, configServerPort = 3000 , configServerPort = 3000
, configServerUnixSocket = Nothing , configServerUnixSocket = Nothing
, configServerUnixSocketMode = Right 432 , configServerUnixSocketMode = 432
, configDbTxAllowOverride = True , configDbTxAllowOverride = True
, configDbTxRollbackAll = True , configDbTxRollbackAll = True
} }
-1
View File
@@ -173,5 +173,4 @@ invalidroleclaimkeys:
- '.role##' - '.role##'
- '.my_role;;domain' - '.my_role;;domain'
- '.#$$%&$%/' - '.#$$%&$%/'
- ''
- '1234' - '1234'